summaryrefslogtreecommitdiff
path: root/packages/backend
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2024-06-01 11:27:03 +0900
committerGitHub <noreply@github.com>2024-06-01 11:27:03 +0900
commitfce66b85b603caac79e1bfa87b5f4621b1ba9d4e (patch)
treed22952ee3f8e30057977a99a33823f4d52990fbc /packages/backend
parentMerge pull request #13493 from misskey-dev/develop (diff)
parentfix(backend): use insertOne insteadof insert/findOneOrFail combination (#13908) (diff)
downloadmisskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.tar.gz
misskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.tar.bz2
misskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.zip
Merge pull request #13917 from misskey-dev/develop
Release 2024.5.0 (master)
Diffstat (limited to 'packages/backend')
-rw-r--r--packages/backend/.swcrc3
-rw-r--r--packages/backend/assets/redoc.html2
-rw-r--r--packages/backend/generate_api_json.js8
-rw-r--r--packages/backend/migration/1689325027964-UserBlacklistAnntena.js5
-rw-r--r--packages/backend/migration/1690417561185-fix-renote-muting.js5
-rw-r--r--packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js5
-rw-r--r--packages/backend/migration/1690417561187-Fix.js5
-rw-r--r--packages/backend/migration/1690569881926-user-2fa-backup-codes.js5
-rw-r--r--packages/backend/migration/1691649257651-refine-announcement.js5
-rw-r--r--packages/backend/migration/1691657412740-refine-announcement-2.js5
-rw-r--r--packages/backend/migration/1695260774117-verified-links.js5
-rw-r--r--packages/backend/migration/1695288787870-following-notify.js5
-rw-r--r--packages/backend/migration/1695440131671-short-name.js5
-rw-r--r--packages/backend/migration/1695605508898-mutingNotificationTypes.js5
-rw-r--r--packages/backend/migration/1695901659683-note-updated-at.js5
-rw-r--r--packages/backend/migration/1696323464251-user-list-membership.js5
-rw-r--r--packages/backend/migration/1696331570827-hibernation.js5
-rw-r--r--packages/backend/migration/1696332072038-clean.js5
-rw-r--r--packages/backend/migration/1700383825690-hard-mute.js5
-rw-r--r--packages/backend/migration/1710512074000-url-preview-meta.js42
-rw-r--r--packages/backend/migration/1710919614510-antenna-exclude-bots.js16
-rw-r--r--packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js21
-rw-r--r--packages/backend/migration/1716345015347-NotRespondingSince.js16
-rw-r--r--packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js50
-rw-r--r--packages/backend/migration/1716450883149-RemoveAntennaNotify.js16
-rw-r--r--packages/backend/migration/1717117195275-inquiryUrl.js16
-rw-r--r--packages/backend/package.json128
-rw-r--r--packages/backend/scripts/check_connect.js (renamed from packages/backend/check_connect.js)2
-rw-r--r--packages/backend/scripts/dev.mjs61
-rw-r--r--packages/backend/scripts/generate_api_json.js13
-rw-r--r--packages/backend/scripts/watch.mjs (renamed from packages/backend/watch.mjs)0
-rw-r--r--packages/backend/src/boot/entry.ts3
-rw-r--r--packages/backend/src/boot/master.ts20
-rw-r--r--packages/backend/src/boot/ready.ts6
-rw-r--r--packages/backend/src/config.ts7
-rw-r--r--packages/backend/src/core/AccountMoveService.ts4
-rw-r--r--packages/backend/src/core/AnnouncementService.ts53
-rw-r--r--packages/backend/src/core/AntennaService.ts6
-rw-r--r--packages/backend/src/core/AvatarDecorationService.ts4
-rw-r--r--packages/backend/src/core/ChannelFollowingService.ts5
-rw-r--r--packages/backend/src/core/ClipService.ts4
-rw-r--r--packages/backend/src/core/CoreModule.ts18
-rw-r--r--packages/backend/src/core/CustomEmojiService.ts13
-rw-r--r--packages/backend/src/core/DriveService.ts20
-rw-r--r--packages/backend/src/core/FanoutTimelineEndpointService.ts19
-rw-r--r--packages/backend/src/core/FederatedInstanceService.ts4
-rw-r--r--packages/backend/src/core/FetchInstanceMetadataService.ts30
-rw-r--r--packages/backend/src/core/FileInfoService.ts49
-rw-r--r--packages/backend/src/core/MfmService.ts13
-rw-r--r--packages/backend/src/core/NoteCreateService.ts24
-rw-r--r--packages/backend/src/core/NoteDeleteService.ts4
-rw-r--r--packages/backend/src/core/PushNotificationService.ts2
-rw-r--r--packages/backend/src/core/RelayService.ts4
-rw-r--r--packages/backend/src/core/ReversiService.ts7
-rw-r--r--packages/backend/src/core/RoleService.ts42
-rw-r--r--packages/backend/src/core/UserFollowingService.ts10
-rw-r--r--packages/backend/src/core/WebAuthnService.ts22
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts117
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts46
-rw-r--r--packages/backend/src/core/activitypub/JsonLdService.ts (renamed from packages/backend/src/core/activitypub/LdSignatureService.ts)32
-rw-r--r--packages/backend/src/core/activitypub/misc/contexts.ts39
-rw-r--r--packages/backend/src/core/activitypub/models/ApImageService.ts19
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts31
-rw-r--r--packages/backend/src/core/activitypub/type.ts12
-rw-r--r--packages/backend/src/core/chart/core.ts37
-rw-r--r--packages/backend/src/core/entities/AbuseUserReportEntityService.ts34
-rw-r--r--packages/backend/src/core/entities/AnnouncementEntityService.ts71
-rw-r--r--packages/backend/src/core/entities/AntennaEntityService.ts3
-rw-r--r--packages/backend/src/core/entities/BlockingEntityService.ts14
-rw-r--r--packages/backend/src/core/entities/ClipEntityService.ts18
-rw-r--r--packages/backend/src/core/entities/DriveFileEntityService.ts12
-rw-r--r--packages/backend/src/core/entities/FlashEntityService.ts14
-rw-r--r--packages/backend/src/core/entities/FollowRequestEntityService.ts27
-rw-r--r--packages/backend/src/core/entities/FollowingEntityService.ts24
-rw-r--r--packages/backend/src/core/entities/GalleryPostEntityService.ts12
-rw-r--r--packages/backend/src/core/entities/InstanceEntityService.ts3
-rw-r--r--packages/backend/src/core/entities/InviteCodeEntityService.ts25
-rw-r--r--packages/backend/src/core/entities/MetaEntityService.ts2
-rw-r--r--packages/backend/src/core/entities/ModerationLogEntityService.ts17
-rw-r--r--packages/backend/src/core/entities/MutingEntityService.ts14
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts13
-rw-r--r--packages/backend/src/core/entities/NoteReactionEntityService.ts11
-rw-r--r--packages/backend/src/core/entities/PageEntityService.ts12
-rw-r--r--packages/backend/src/core/entities/RenoteMutingEntityService.ts14
-rw-r--r--packages/backend/src/core/entities/ReversiGameEntityService.ts66
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts249
-rw-r--r--packages/backend/src/core/entities/UserListEntityService.ts5
-rw-r--r--packages/backend/src/misc/fastify-hook-handlers.ts5
-rw-r--r--packages/backend/src/misc/gen-identicon.ts9
-rw-r--r--packages/backend/src/misc/is-pure-renote.ts10
-rw-r--r--packages/backend/src/misc/is-quote.ts12
-rw-r--r--packages/backend/src/misc/is-renote.ts67
-rw-r--r--packages/backend/src/misc/json-schema.ts4
-rw-r--r--packages/backend/src/misc/loader.ts5
-rw-r--r--packages/backend/src/models/Antenna.ts8
-rw-r--r--packages/backend/src/models/Instance.ts17
-rw-r--r--packages/backend/src/models/Meta.ts44
-rw-r--r--packages/backend/src/models/Poll.ts9
-rw-r--r--packages/backend/src/models/RepositoryModule.ts136
-rw-r--r--packages/backend/src/models/ReversiGame.ts5
-rw-r--r--packages/backend/src/models/Role.ts85
-rw-r--r--packages/backend/src/models/_.ts205
-rw-r--r--packages/backend/src/models/json-schema/antenna.ts8
-rw-r--r--packages/backend/src/models/json-schema/clip.ts4
-rw-r--r--packages/backend/src/models/json-schema/federation-instance.ts5
-rw-r--r--packages/backend/src/models/json-schema/meta.ts8
-rw-r--r--packages/backend/src/models/json-schema/note.ts4
-rw-r--r--packages/backend/src/models/json-schema/role.ts17
-rw-r--r--packages/backend/src/models/json-schema/signin.ts5
-rw-r--r--packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/DeliverProcessorService.ts14
-rw-r--r--packages/backend/src/queue/processors/ExportAntennasProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/ImportAntennasProcessorService.ts10
-rw-r--r--packages/backend/src/queue/processors/ImportUserListsProcessorService.ts4
-rw-r--r--packages/backend/src/queue/processors/InboxProcessorService.ts59
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts4
-rw-r--r--packages/backend/src/server/FileServerService.ts7
-rw-r--r--packages/backend/src/server/HealthServerService.ts54
-rw-r--r--packages/backend/src/server/NodeinfoServerService.ts13
-rw-r--r--packages/backend/src/server/ServerModule.ts2
-rw-r--r--packages/backend/src/server/ServerService.ts22
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts77
-rw-r--r--packages/backend/src/server/api/ApiServerService.ts2
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts4
-rw-r--r--packages/backend/src/server/api/SigninService.ts4
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts4
-rw-r--r--packages/backend/src/server/api/endpoints.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/ad/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/admin/invite/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/users.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-users.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts46
-rw-r--r--packages/backend/src/server/api/endpoints/announcements.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/announcements/show.ts54
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/create.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/update.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/app/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/auth/session/generate.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/channels/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/find.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/fetch-rss.ts179
-rw-r--r--packages/backend/src/server/api/endpoints/flash/create.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/following/requests/list.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-blocking.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-following.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-muting.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-user-lists.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts60
-rw-r--r--packages/backend/src/server/api/endpoints/i/update-email.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts36
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/invite/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/notes/polls/vote.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/notes/reactions.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/translate.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/pages/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/roles/users.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/users/following.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/users/relation.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/users/report-abuse.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.ts10
-rw-r--r--packages/backend/src/server/api/openapi/gen-spec.ts2
-rw-r--r--packages/backend/src/server/api/stream/channel.ts22
-rw-r--r--packages/backend/src/server/api/stream/channels/antenna.ts8
-rw-r--r--packages/backend/src/server/api/stream/channels/channel.ts11
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts25
-rw-r--r--packages/backend/src/server/api/stream/channels/hashtag.ts11
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts18
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts16
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts14
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts9
-rw-r--r--packages/backend/src/server/api/stream/channels/user-list.ts17
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts36
-rw-r--r--packages/backend/src/server/web/UrlPreviewService.ts67
-rw-r--r--packages/backend/src/server/web/boot.js4
-rw-r--r--packages/backend/src/server/web/views/base.pug5
-rw-r--r--packages/backend/src/server/web/views/note.pug4
-rw-r--r--packages/backend/src/server/web/views/page.pug2
-rw-r--r--packages/backend/src/server/web/views/user.pug2
-rw-r--r--packages/backend/test/e2e/2fa.ts96
-rw-r--r--packages/backend/test/e2e/antennas.ts119
-rw-r--r--packages/backend/test/e2e/api-visibility.ts74
-rw-r--r--packages/backend/test/e2e/api.ts43
-rw-r--r--packages/backend/test/e2e/block.ts19
-rw-r--r--packages/backend/test/e2e/clips.ts298
-rw-r--r--packages/backend/test/e2e/drive.ts36
-rw-r--r--packages/backend/test/e2e/endpoints.ts261
-rw-r--r--packages/backend/test/e2e/exports.ts38
-rw-r--r--packages/backend/test/e2e/fetch-resource.ts18
-rw-r--r--packages/backend/test/e2e/ff-visibility.ts206
-rw-r--r--packages/backend/test/e2e/move.ts192
-rw-r--r--packages/backend/test/e2e/mute.ts87
-rw-r--r--packages/backend/test/e2e/note.ts284
-rw-r--r--packages/backend/test/e2e/renote-mute.ts35
-rw-r--r--packages/backend/test/e2e/reversi-game.ts33
-rw-r--r--packages/backend/test/e2e/streaming.ts24
-rw-r--r--packages/backend/test/e2e/thread-mute.ts18
-rw-r--r--packages/backend/test/e2e/timelines.ts354
-rw-r--r--packages/backend/test/e2e/user-notes.ts8
-rw-r--r--packages/backend/test/e2e/users.ts461
-rw-r--r--packages/backend/test/global.d.ts7
-rw-r--r--packages/backend/test/jest.setup.ts5
-rw-r--r--packages/backend/test/prelude/get-api-validator.ts4
-rw-r--r--packages/backend/test/resources/kick_gaba7.m4abin0 -> 9817 bytes
-rw-r--r--packages/backend/test/tsconfig.json3
-rw-r--r--packages/backend/test/unit/AnnouncementService.ts4
-rw-r--r--packages/backend/test/unit/ApMfmService.ts5
-rw-r--r--packages/backend/test/unit/FetchInstanceMetadataService.ts43
-rw-r--r--packages/backend/test/unit/FileInfoService.ts27
-rw-r--r--packages/backend/test/unit/MfmService.ts6
-rw-r--r--packages/backend/test/unit/NoteCreateService.ts144
-rw-r--r--packages/backend/test/unit/RelayService.ts3
-rw-r--r--packages/backend/test/unit/RoleService.ts495
-rw-r--r--packages/backend/test/unit/activitypub.ts68
-rw-r--r--packages/backend/test/unit/entities/UserEntityService.ts528
-rw-r--r--packages/backend/test/unit/misc/is-renote.ts88
-rw-r--r--packages/backend/test/unit/misc/loader.ts5
-rw-r--r--packages/backend/test/utils.ts61
228 files changed, 5549 insertions, 2369 deletions
diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc
index 0504a2d389..845190b5f4 100644
--- a/packages/backend/.swcrc
+++ b/packages/backend/.swcrc
@@ -19,5 +19,6 @@
},
"target": "es2022"
},
- "minify": false
+ "minify": false,
+ "sourceMaps": "inline"
}
diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html
index a9ebf662fc..2557b4532e 100644
--- a/packages/backend/assets/redoc.html
+++ b/packages/backend/assets/redoc.html
@@ -19,6 +19,6 @@
</head>
<body>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
- <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
+ <script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
</body>
</html>
diff --git a/packages/backend/generate_api_json.js b/packages/backend/generate_api_json.js
deleted file mode 100644
index 4079b3bb0a..0000000000
--- a/packages/backend/generate_api_json.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { loadConfig } from './built/config.js'
-import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
-import { writeFileSync } from "node:fs";
-
-const config = loadConfig();
-const spec = genOpenapiSpec(config, true);
-
-writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); \ No newline at end of file
diff --git a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
index ce246b20f8..2dc7774493 100644
--- a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
+++ b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class UserBlacklistAnntena1689325027964 {
name = 'UserBlacklistAnntena1689325027964'
diff --git a/packages/backend/migration/1690417561185-fix-renote-muting.js b/packages/backend/migration/1690417561185-fix-renote-muting.js
index 14150b0362..d9604ca26c 100644
--- a/packages/backend/migration/1690417561185-fix-renote-muting.js
+++ b/packages/backend/migration/1690417561185-fix-renote-muting.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class FixRenoteMuting1690417561185 {
name = 'FixRenoteMuting1690417561185'
diff --git a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
index 7eda5debe5..9bccdb3bb5 100644
--- a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
+++ b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class ChangeCacheRemoteFilesDefault1690417561186 {
name = 'ChangeCacheRemoteFilesDefault1690417561186'
diff --git a/packages/backend/migration/1690417561187-Fix.js b/packages/backend/migration/1690417561187-Fix.js
index e780e66d7b..7f1d62d68c 100644
--- a/packages/backend/migration/1690417561187-Fix.js
+++ b/packages/backend/migration/1690417561187-Fix.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class Fix1690417561187 {
name = 'Fix1690417561187'
diff --git a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
index 2049df8ea2..a3ef8dcf08 100644
--- a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
+++ b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class User2faBackupCodes1690569881926 {
name = 'User2faBackupCodes1690569881926'
diff --git a/packages/backend/migration/1691649257651-refine-announcement.js b/packages/backend/migration/1691649257651-refine-announcement.js
index d8d63f3103..ac621155d5 100644
--- a/packages/backend/migration/1691649257651-refine-announcement.js
+++ b/packages/backend/migration/1691649257651-refine-announcement.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class RefineAnnouncement1691649257651 {
name = 'RefineAnnouncement1691649257651'
diff --git a/packages/backend/migration/1691657412740-refine-announcement-2.js b/packages/backend/migration/1691657412740-refine-announcement-2.js
index 8791f99f44..67edf19659 100644
--- a/packages/backend/migration/1691657412740-refine-announcement-2.js
+++ b/packages/backend/migration/1691657412740-refine-announcement-2.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class RefineAnnouncement21691657412740 {
name = 'RefineAnnouncement21691657412740'
diff --git a/packages/backend/migration/1695260774117-verified-links.js b/packages/backend/migration/1695260774117-verified-links.js
index 18e0571d81..64c8a9ad8f 100644
--- a/packages/backend/migration/1695260774117-verified-links.js
+++ b/packages/backend/migration/1695260774117-verified-links.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class VerifiedLinks1695260774117 {
name = 'VerifiedLinks1695260774117'
diff --git a/packages/backend/migration/1695288787870-following-notify.js b/packages/backend/migration/1695288787870-following-notify.js
index e7e2194b15..b3f78d5f2a 100644
--- a/packages/backend/migration/1695288787870-following-notify.js
+++ b/packages/backend/migration/1695288787870-following-notify.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class FollowingNotify1695288787870 {
name = 'FollowingNotify1695288787870'
diff --git a/packages/backend/migration/1695440131671-short-name.js b/packages/backend/migration/1695440131671-short-name.js
index 2c37297fc1..fdc256caf8 100644
--- a/packages/backend/migration/1695440131671-short-name.js
+++ b/packages/backend/migration/1695440131671-short-name.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class ShortName1695440131671 {
name = 'ShortName1695440131671'
diff --git a/packages/backend/migration/1695605508898-mutingNotificationTypes.js b/packages/backend/migration/1695605508898-mutingNotificationTypes.js
index 8c0e52a2f0..67d4243142 100644
--- a/packages/backend/migration/1695605508898-mutingNotificationTypes.js
+++ b/packages/backend/migration/1695605508898-mutingNotificationTypes.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class MutingNotificationTypes1695605508898 {
name = 'MutingNotificationTypes1695605508898'
diff --git a/packages/backend/migration/1695901659683-note-updated-at.js b/packages/backend/migration/1695901659683-note-updated-at.js
index d8a151a1f7..e828fb1a6f 100644
--- a/packages/backend/migration/1695901659683-note-updated-at.js
+++ b/packages/backend/migration/1695901659683-note-updated-at.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class NoteUpdatedAt1695901659683 {
name = 'NoteUpdatedAt1695901659683'
diff --git a/packages/backend/migration/1696323464251-user-list-membership.js b/packages/backend/migration/1696323464251-user-list-membership.js
index 7534040c4c..dc1d438dd7 100644
--- a/packages/backend/migration/1696323464251-user-list-membership.js
+++ b/packages/backend/migration/1696323464251-user-list-membership.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class UserListMembership1696323464251 {
name = 'UserListMembership1696323464251'
diff --git a/packages/backend/migration/1696331570827-hibernation.js b/packages/backend/migration/1696331570827-hibernation.js
index 119d35913f..1487ece77c 100644
--- a/packages/backend/migration/1696331570827-hibernation.js
+++ b/packages/backend/migration/1696331570827-hibernation.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class Hibernation1696331570827 {
name = 'Hibernation1696331570827'
diff --git a/packages/backend/migration/1696332072038-clean.js b/packages/backend/migration/1696332072038-clean.js
index 97dba655f4..92a6810d6a 100644
--- a/packages/backend/migration/1696332072038-clean.js
+++ b/packages/backend/migration/1696332072038-clean.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class Clean1696332072038 {
name = 'Clean1696332072038'
diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js
index afd3247f5c..92c3ada4a1 100644
--- a/packages/backend/migration/1700383825690-hard-mute.js
+++ b/packages/backend/migration/1700383825690-hard-mute.js
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export class HardMute1700383825690 {
name = 'HardMute1700383825690'
diff --git a/packages/backend/migration/1710512074000-url-preview-meta.js b/packages/backend/migration/1710512074000-url-preview-meta.js
new file mode 100644
index 0000000000..8af521bbf4
--- /dev/null
+++ b/packages/backend/migration/1710512074000-url-preview-meta.js
@@ -0,0 +1,42 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class UrlPreviewMeta1710512074000 {
+ name = 'UrlPreviewMeta1710512074000'
+
+ async up(queryRunner) {
+ await queryRunner.query(`
+ alter table meta
+ rename column "summalyProxy" to "urlPreviewSummaryProxyUrl";
+ alter table meta
+ add "urlPreviewEnabled" boolean default true not null;
+ alter table meta
+ add "urlPreviewTimeout" integer default 10000 not null;
+ alter table meta
+ add "urlPreviewMaximumContentLength" bigint default 10485760 not null;
+ alter table meta
+ add "urlPreviewRequireContentLength" boolean default false not null;
+ alter table meta
+ add "urlPreviewUserAgent" varchar(1024) default null;
+ `);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`
+ alter table meta
+ rename column "urlPreviewSummaryProxyUrl" to "summalyProxy";
+ alter table meta
+ drop column "urlPreviewEnabled";
+ alter table meta
+ drop column "urlPreviewTimeout";
+ alter table meta
+ drop column "urlPreviewMaximumContentLength";
+ alter table meta
+ drop column "urlPreviewRequireContentLength";
+ alter table meta
+ drop column "urlPreviewUserAgent";
+ `);
+ }
+}
diff --git a/packages/backend/migration/1710919614510-antenna-exclude-bots.js b/packages/backend/migration/1710919614510-antenna-exclude-bots.js
new file mode 100644
index 0000000000..fac84317cc
--- /dev/null
+++ b/packages/backend/migration/1710919614510-antenna-exclude-bots.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AntennaExcludeBots1710919614510 {
+ name = 'AntennaExcludeBots1710919614510'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeBots" boolean NOT NULL DEFAULT false`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeBots"`);
+ }
+}
diff --git a/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js b/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js
new file mode 100644
index 0000000000..f736378c04
--- /dev/null
+++ b/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class ChannelIdDenormalizedForMiPoll1716129964060 {
+ name = 'ChannelIdDenormalizedForMiPoll1716129964060'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "poll" ADD "channelId" character varying(32)`);
+ await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`);
+ await queryRunner.query(`CREATE INDEX "IDX_c1240fcc9675946ea5d6c2860e" ON "poll" ("channelId") `);
+ await queryRunner.query(`UPDATE "poll" SET "channelId" = "note"."channelId" FROM "note" WHERE "poll"."noteId" = "note"."id"`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`DROP INDEX "public"."IDX_c1240fcc9675946ea5d6c2860e"`);
+ await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`);
+ await queryRunner.query(`ALTER TABLE "poll" DROP COLUMN "channelId"`);
+ }
+}
diff --git a/packages/backend/migration/1716345015347-NotRespondingSince.js b/packages/backend/migration/1716345015347-NotRespondingSince.js
new file mode 100644
index 0000000000..fc4ee6639a
--- /dev/null
+++ b/packages/backend/migration/1716345015347-NotRespondingSince.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class NotRespondingSince1716345015347 {
+ name = 'NotRespondingSince1716345015347'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "instance" ADD "notRespondingSince" TIMESTAMP WITH TIME ZONE`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "notRespondingSince"`);
+ }
+}
diff --git a/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js b/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js
new file mode 100644
index 0000000000..4808a9a3db
--- /dev/null
+++ b/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class SuspensionStateInsteadOfIsSspended1716345771510 {
+ name = 'SuspensionStateInsteadOfIsSspended1716345771510'
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE TYPE "public"."instance_suspensionstate_enum" AS ENUM('none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding')`);
+
+ await queryRunner.query(`DROP INDEX "public"."IDX_34500da2e38ac393f7bb6b299c"`);
+
+ await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "isSuspended" TO "suspensionState"`);
+
+ await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
+
+ await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE "public"."instance_suspensionstate_enum" USING (
+ CASE "suspensionState"
+ WHEN TRUE THEN 'manuallySuspended'::instance_suspensionstate_enum
+ ELSE 'none'::instance_suspensionstate_enum
+ END
+ )`);
+
+ await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT 'none'`);
+
+ await queryRunner.query(`CREATE INDEX "IDX_3ede46f507c87ad698051d56a8" ON "instance" ("suspensionState") `);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`DROP INDEX "public"."IDX_3ede46f507c87ad698051d56a8"`);
+
+ await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
+
+ await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE boolean USING (
+ CASE "suspensionState"
+ WHEN 'none'::instance_suspensionstate_enum THEN FALSE
+ ELSE TRUE
+ END
+ )`);
+
+ await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT false`);
+
+ await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "suspensionState" TO "isSuspended"`);
+
+ await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `);
+
+ await queryRunner.query(`DROP TYPE "public"."instance_suspensionstate_enum"`);
+ }
+}
diff --git a/packages/backend/migration/1716450883149-RemoveAntennaNotify.js b/packages/backend/migration/1716450883149-RemoveAntennaNotify.js
new file mode 100644
index 0000000000..b5a2441855
--- /dev/null
+++ b/packages/backend/migration/1716450883149-RemoveAntennaNotify.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RemoveAntennaNotify1716450883149 {
+ name = 'RemoveAntennaNotify1716450883149'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "notify"`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "antenna" ADD "notify" boolean NOT NULL`);
+ }
+}
diff --git a/packages/backend/migration/1717117195275-inquiryUrl.js b/packages/backend/migration/1717117195275-inquiryUrl.js
new file mode 100644
index 0000000000..29ca31af14
--- /dev/null
+++ b/packages/backend/migration/1717117195275-inquiryUrl.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class InquiryUrl1717117195275 {
+ name = 'InquiryUrl1717117195275'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "inquiryUrl" character varying(1024)`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "inquiryUrl"`);
+ }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 8680610441..e034f75dc5 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -4,22 +4,22 @@
"private": true,
"type": "module",
"engines": {
- "node": ">=20.10.0"
+ "node": "^20.10.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
- "check:connect": "node ./check_connect.js",
- "build": "swc src -d built -D",
- "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
- "watch:swc": "swc src -d built -D -w",
+ "check:connect": "node ./scripts/check_connect.js",
+ "build": "swc src -d built -D --strip-leading-paths",
+ "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
+ "watch:swc": "swc src -d built -D -w --strip-leading-paths",
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
- "watch": "node watch.mjs",
+ "watch": "node ./scripts/watch.mjs",
"restart": "pnpm build && pnpm start",
- "dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"",
- "typecheck": "tsc --noEmit",
+ "dev": "node ./scripts/dev.mjs",
+ "typecheck": "tsc --noEmit && tsc -p test --noEmit",
"eslint": "eslint --quiet \"src/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
@@ -31,7 +31,7 @@
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
"test-and-coverage": "pnpm jest-and-coverage",
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
- "generate-api-json": "pnpm build && node ./generate_api_json.js"
+ "generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
@@ -67,38 +67,41 @@
"dependencies": {
"@aws-sdk/client-s3": "3.412.0",
"@aws-sdk/lib-storage": "3.412.0",
- "@bull-board/api": "5.14.2",
- "@bull-board/fastify": "5.14.2",
- "@bull-board/ui": "5.14.2",
- "@discordapp/twemoji": "15.0.2",
+ "@bull-board/api": "5.17.0",
+ "@bull-board/fastify": "5.17.0",
+ "@bull-board/ui": "5.17.0",
+ "@discordapp/twemoji": "15.0.3",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1",
- "@fastify/cors": "8.5.0",
- "@fastify/express": "2.3.0",
- "@fastify/http-proxy": "9.3.0",
- "@fastify/multipart": "8.1.0",
- "@fastify/static": "6.12.0",
- "@fastify/view": "8.2.0",
+ "@fastify/cors": "9.0.1",
+ "@fastify/express": "3.0.0",
+ "@fastify/http-proxy": "9.5.0",
+ "@fastify/multipart": "8.2.0",
+ "@fastify/static": "7.0.3",
+ "@fastify/view": "9.1.0",
"@misskey-dev/sharp-read-bmp": "1.2.0",
- "@misskey-dev/summaly": "5.0.3",
- "@nestjs/common": "10.3.3",
- "@nestjs/core": "10.3.3",
- "@nestjs/testing": "10.3.3",
+ "@misskey-dev/summaly": "5.1.0",
+ "@napi-rs/canvas": "^0.1.52",
+ "@nestjs/common": "10.3.8",
+ "@nestjs/core": "10.3.8",
+ "@nestjs/testing": "10.3.8",
"@peertube/http-signature": "1.7.0",
- "@simplewebauthn/server": "9.0.3",
+ "@sentry/node": "^8.5.0",
+ "@sentry/profiling-node": "^8.5.0",
+ "@simplewebauthn/server": "10.0.0",
"@sinonjs/fake-timers": "11.2.2",
- "@smithy/node-http-handler": "2.1.10",
- "@swc/cli": "0.1.63",
- "@swc/core": "1.3.107",
- "@twemoji/parser": "15.0.0",
+ "@smithy/node-http-handler": "2.5.0",
+ "@swc/cli": "0.3.12",
+ "@swc/core": "1.4.17",
+ "@twemoji/parser": "15.1.1",
"accepts": "1.3.8",
- "ajv": "8.12.0",
- "archiver": "6.0.1",
- "async-mutex": "0.4.1",
+ "ajv": "8.13.0",
+ "archiver": "7.0.1",
+ "async-mutex": "0.5.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
- "bullmq": "5.4.0",
+ "bullmq": "5.7.8",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@@ -109,85 +112,84 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
- "fastify": "4.25.2",
+ "fastify": "4.26.2",
"fastify-raw-body": "4.3.0",
"feed": "4.2.2",
"file-type": "19.0.0",
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
- "got": "14.2.0",
+ "got": "14.2.1",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
- "http-link-header": "1.1.2",
- "ioredis": "5.3.2",
+ "http-link-header": "1.1.3",
+ "ioredis": "5.4.1",
"ip-cidr": "3.1.0",
- "ipaddr.js": "2.1.0",
+ "ipaddr.js": "2.2.0",
"is-svg": "5.0.0",
"js-yaml": "4.1.0",
- "jsdom": "23.2.0",
+ "jsdom": "24.0.0",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
- "meilisearch": "0.37.0",
+ "meilisearch": "0.38.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1",
- "nanoid": "5.0.6",
+ "nanoid": "5.0.7",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
- "nodemailer": "6.9.10",
+ "nodemailer": "6.9.13",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
- "otpauth": "9.2.2",
+ "otpauth": "9.2.3",
"parse5": "7.1.2",
- "pg": "8.11.3",
+ "pg": "8.11.5",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
"punycode": "2.3.1",
- "pureimage": "0.3.17",
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
- "re2": "1.20.9",
+ "re2": "1.20.10",
"redis-lock": "0.1.4",
- "reflect-metadata": "0.2.1",
+ "reflect-metadata": "0.2.2",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",
- "sanitize-html": "2.12.1",
+ "sanitize-html": "2.13.0",
"secure-json-parse": "2.7.0",
- "sharp": "0.33.2",
+ "sharp": "0.33.3",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
- "systeminformation": "5.22.0",
+ "systeminformation": "5.22.7",
"tinycolor2": "1.6.0",
- "tmp": "0.2.2",
+ "tmp": "0.2.3",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
- "typescript": "5.3.3",
+ "typescript": "5.4.5",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
- "ws": "8.16.0",
+ "ws": "8.17.0",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0",
- "@nestjs/platform-express": "10.3.3",
- "@simplewebauthn/types": "9.0.1",
- "@swc/jest": "0.2.31",
+ "@nestjs/platform-express": "10.3.8",
+ "@simplewebauthn/types": "10.0.0",
+ "@swc/jest": "0.2.36",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.2",
"@types/bcryptjs": "2.4.6",
@@ -197,20 +199,20 @@
"@types/fluent-ffmpeg": "2.1.24",
"@types/htmlescape": "^1.1.3",
"@types/http-link-header": "1.0.5",
- "@types/jest": "29.5.11",
+ "@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.6",
"@types/jsonld": "1.5.13",
- "@types/jsrsasign": "10.5.12",
+ "@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
- "@types/node": "20.11.22",
+ "@types/node": "20.12.7",
"@types/node-fetch": "3.0.3",
- "@types/nodemailer": "6.4.14",
+ "@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4",
- "@types/oauth2orize": "1.11.3",
+ "@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
- "@types/pg": "8.11.2",
+ "@types/pg": "8.11.5",
"@types/pug": "2.0.10",
"@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5",
@@ -226,8 +228,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/ws": "8.5.10",
- "@typescript-eslint/eslint-plugin": "7.1.0",
- "@typescript-eslint/parser": "7.1.0",
+ "@typescript-eslint/eslint-plugin": "7.7.1",
+ "@typescript-eslint/parser": "7.7.1",
"aws-sdk-client-mock": "3.0.1",
"cross-env": "7.0.3",
"eslint": "8.57.0",
diff --git a/packages/backend/check_connect.js b/packages/backend/scripts/check_connect.js
index d88e649c09..ba25fd416c 100644
--- a/packages/backend/check_connect.js
+++ b/packages/backend/scripts/check_connect.js
@@ -4,7 +4,7 @@
*/
import Redis from 'ioredis';
-import { loadConfig } from './built/config.js';
+import { loadConfig } from '../built/config.js';
const config = loadConfig();
const redis = new Redis(config.redis);
diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs
new file mode 100644
index 0000000000..2d0de0f916
--- /dev/null
+++ b/packages/backend/scripts/dev.mjs
@@ -0,0 +1,61 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { execa, execaNode } from 'execa';
+
+/** @type {import('execa').ExecaChildProcess | undefined} */
+let backendProcess;
+
+async function execBuildAssets() {
+ await execa('pnpm', ['run', 'build-assets'], {
+ cwd: '../../',
+ stdout: process.stdout,
+ stderr: process.stderr,
+ })
+}
+
+function execStart() {
+ // pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので
+ // 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい
+ backendProcess = execaNode('./built/boot/entry.js', [], {
+ stdout: process.stdout,
+ stderr: process.stderr,
+ env: {
+ 'NODE_ENV': 'development',
+ },
+ });
+}
+
+async function killProc() {
+ if (backendProcess) {
+ backendProcess.kill();
+ await new Promise(resolve => backendProcess.on('exit', resolve));
+ backendProcess = undefined;
+ }
+}
+
+(async () => {
+ execaNode(
+ './node_modules/nodemon/bin/nodemon.js',
+ [
+ '-w', 'src',
+ '-e', 'ts,js,mjs,cjs,json',
+ '--exec', 'pnpm', 'run', 'build',
+ ],
+ {
+ stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
+ })
+ .on('message', async (message) => {
+ if (message.type === 'exit') {
+ // かならずbuild->build-assetsの順番で呼び出したいので、
+ // 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。
+ // pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある
+
+ await killProc();
+ await execBuildAssets();
+ execStart();
+ }
+ })
+})();
diff --git a/packages/backend/scripts/generate_api_json.js b/packages/backend/scripts/generate_api_json.js
new file mode 100644
index 0000000000..b4769ef801
--- /dev/null
+++ b/packages/backend/scripts/generate_api_json.js
@@ -0,0 +1,13 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { loadConfig } from '../built/config.js'
+import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js'
+import { writeFileSync } from "node:fs";
+
+const config = loadConfig();
+const spec = genOpenapiSpec(config, true);
+
+writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
diff --git a/packages/backend/watch.mjs b/packages/backend/scripts/watch.mjs
index a0ccea3b16..a0ccea3b16 100644
--- a/packages/backend/watch.mjs
+++ b/packages/backend/scripts/watch.mjs
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index 6b8e83d4f9..04c6ca9723 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -15,6 +15,7 @@ import Logger from '@/logger.js';
import { envOption } from '../env.js';
import { masterMain } from './master.js';
import { workerMain } from './worker.js';
+import { readyRef } from './ready.js';
import 'reflect-metadata';
@@ -79,6 +80,8 @@ if (cluster.isWorker || envOption.disableClustering) {
await workerMain();
}
+readyRef.value = true;
+
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
// それ以外のときは process.send は使えないので弾く
if (process.send) {
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 30f9477ccf..75e1a80cd1 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -10,6 +10,8 @@ import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
import Logger from '@/logger.js';
import { loadConfig } from '@/config.js';
import type { Config } from '@/config.js';
@@ -71,6 +73,24 @@ export async function masterMain() {
bootLogger.succ('Misskey initialized');
+ if (config.sentryForBackend) {
+ Sentry.init({
+ integrations: [
+ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+ ],
+
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
+ profilesSampleRate: 1.0,
+
+ maxBreadcrumbs: 0,
+
+ ...config.sentryForBackend.options,
+ });
+ }
+
if (envOption.disableClustering) {
if (envOption.onlyServer) {
await server();
diff --git a/packages/backend/src/boot/ready.ts b/packages/backend/src/boot/ready.ts
new file mode 100644
index 0000000000..591ae5cb58
--- /dev/null
+++ b/packages/backend/src/boot/ready.ts
@@ -0,0 +1,6 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const readyRef = { value: false };
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 0ca1fa55c1..0ac521d409 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -7,6 +7,7 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
+import * as Sentry from '@sentry/node';
import type { RedisOptions } from 'ioredis';
type RedisOptionsSource = Partial<RedisOptions> & {
@@ -56,6 +57,8 @@ type Source = {
index: string;
scope?: 'local' | 'global' | string[];
};
+ sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
+ sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
@@ -166,6 +169,8 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
+ sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
+ sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
@@ -234,6 +239,8 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
+ sentryForBackend: config.sentryForBackend,
+ sentryForFrontend: config.sentryForFrontend,
id: config.id,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 5bd885df40..b6b591d240 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -305,7 +305,7 @@ export class AccountMoveService {
let resultUser: MiLocalUser | MiRemoteUser | null = null;
if (this.userEntityService.isRemoteUser(dst)) {
- if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
+ if (Date.now() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
await this.apPersonService.updatePerson(dst.uri);
}
dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
@@ -321,7 +321,7 @@ export class AccountMoveService {
if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
if (this.userEntityService.isRemoteUser(dst)) {
- if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
+ if (Date.now() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
await this.apPersonService.updatePerson(srcUri);
}
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index b298a70929..40a9db01c0 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -4,13 +4,14 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { Brackets } from 'typeorm';
+import { Brackets, EntityNotFoundError } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
+import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@@ -29,6 +30,7 @@ export class AnnouncementService {
private idService: IdService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
+ private announcementEntityService: AnnouncementEntityService,
) {
}
@@ -65,7 +67,7 @@ export class AnnouncementService {
@bindThis
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
- const announcement = await this.announcementsRepository.insert({
+ const announcement = await this.announcementsRepository.insertOne({
id: this.idService.gen(),
updatedAt: null,
title: values.title,
@@ -77,9 +79,9 @@ export class AnnouncementService {
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
userId: values.userId,
- }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
+ });
- const packed = (await this.packMany([announcement]))[0];
+ const packed = await this.announcementEntityService.pack(announcement);
if (values.userId) {
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
@@ -178,6 +180,24 @@ export class AnnouncementService {
}
@bindThis
+ public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise<Packed<'Announcement'>> {
+ const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
+ if (me) {
+ if (announcement.userId && announcement.userId !== me.id) {
+ throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
+ }
+
+ const read = await this.announcementReadsRepository.findOneBy({
+ announcementId: announcement.id,
+ userId: me.id,
+ });
+ return this.announcementEntityService.pack({ ...announcement, isRead: read !== null }, me);
+ } else {
+ return this.announcementEntityService.pack(announcement, null);
+ }
+ }
+
+ @bindThis
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
try {
await this.announcementReadsRepository.insert({
@@ -193,29 +213,4 @@ export class AnnouncementService {
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
}
}
-
- @bindThis
- public async packMany(
- announcements: MiAnnouncement[],
- me?: { id: MiUser['id'] } | null | undefined,
- options?: {
- reads?: MiAnnouncementRead[];
- },
- ): Promise<Packed<'Announcement'>[]> {
- const reads = me ? (options?.reads ?? await this.getReads(me.id)) : [];
- return announcements.map(announcement => ({
- id: announcement.id,
- createdAt: this.idService.parse(announcement.id).date.toISOString(),
- updatedAt: announcement.updatedAt?.toISOString() ?? null,
- text: announcement.text,
- title: announcement.title,
- imageUrl: announcement.imageUrl,
- icon: announcement.icon,
- display: announcement.display,
- needConfirmationToRead: announcement.needConfirmationToRead,
- silence: announcement.silence,
- forYou: announcement.userId === me?.id,
- isRead: reads.some(read => read.announcementId === announcement.id),
- }));
- }
}
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 4f956a43ed..793d8974b3 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -92,7 +92,7 @@ export class AntennaService implements OnApplicationShutdown {
}
@bindThis
- public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise<void> {
+ public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<void> {
const antennas = await this.getAntennas();
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
@@ -110,10 +110,12 @@ export class AntennaService implements OnApplicationShutdown {
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
@bindThis
- public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise<boolean> {
+ public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<boolean> {
if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false;
+ if (antenna.excludeBots && noteUser.isBot) return false;
+
if (antenna.localOnly && noteUser.host != null) return false;
if (!antenna.withReplies && note.replyId != null) return false;
diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts
index 21e31d79a4..8b54bbe012 100644
--- a/packages/backend/src/core/AvatarDecorationService.ts
+++ b/packages/backend/src/core/AvatarDecorationService.ts
@@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
@bindThis
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
- const created = await this.avatarDecorationsRepository.insert({
+ const created = await this.avatarDecorationsRepository.insertOne({
id: this.idService.gen(),
...options,
- }).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
diff --git a/packages/backend/src/core/ChannelFollowingService.ts b/packages/backend/src/core/ChannelFollowingService.ts
index 75843b9773..12251595e2 100644
--- a/packages/backend/src/core/ChannelFollowingService.ts
+++ b/packages/backend/src/core/ChannelFollowingService.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts
index bb8be26ce6..9fd1ebad87 100644
--- a/packages/backend/src/core/ClipService.ts
+++ b/packages/backend/src/core/ClipService.ts
@@ -45,13 +45,13 @@ export class ClipService {
throw new ClipService.TooManyClipsError();
}
- const clip = await this.clipsRepository.insert({
+ const clip = await this.clipsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: name,
isPublic: isPublic,
description: description,
- }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
+ });
return clip;
}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 2c27d33c06..be80df6f1c 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -84,6 +84,7 @@ import ApRequestChart from './chart/charts/ap-request.js';
import { ChartManagementService } from './chart/ChartManagementService.js';
import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js';
+import { AnnouncementEntityService } from './entities/AnnouncementEntityService.js';
import { AntennaEntityService } from './entities/AntennaEntityService.js';
import { AppEntityService } from './entities/AppEntityService.js';
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
@@ -127,7 +128,7 @@ import { ApMfmService } from './activitypub/ApMfmService.js';
import { ApRendererService } from './activitypub/ApRendererService.js';
import { ApRequestService } from './activitypub/ApRequestService.js';
import { ApResolverService } from './activitypub/ApResolverService.js';
-import { LdSignatureService } from './activitypub/LdSignatureService.js';
+import { JsonLdService } from './activitypub/JsonLdService.js';
import { RemoteLoggerService } from './RemoteLoggerService.js';
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
import { WebfingerService } from './WebfingerService.js';
@@ -223,6 +224,7 @@ const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRe
const $ChartManagementService: Provider = { provide: 'ChartManagementService', useExisting: ChartManagementService };
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
+const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
@@ -266,7 +268,7 @@ const $ApMfmService: Provider = { provide: 'ApMfmService', useExisting: ApMfmSer
const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService };
const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService };
const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService };
-const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService };
+const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService };
const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService };
const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService };
const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService };
@@ -363,6 +365,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ChartManagementService,
AbuseUserReportEntityService,
+ AnnouncementEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -406,7 +409,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
- LdSignatureService,
+ JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
WebfingerService,
@@ -499,6 +502,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ChartManagementService,
$AbuseUserReportEntityService,
+ $AnnouncementEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -542,7 +546,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ApRendererService,
$ApRequestService,
$ApResolverService,
- $LdSignatureService,
+ $JsonLdService,
$RemoteLoggerService,
$RemoteUserResolveService,
$WebfingerService,
@@ -635,6 +639,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ChartManagementService,
AbuseUserReportEntityService,
+ AnnouncementEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -678,7 +683,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
- LdSignatureService,
+ JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
WebfingerService,
@@ -770,6 +775,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ChartManagementService,
$AbuseUserReportEntityService,
+ $AnnouncementEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -813,7 +819,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ApRendererService,
$ApRequestService,
$ApResolverService,
- $LdSignatureService,
+ $JsonLdService,
$RemoteLoggerService,
$RemoteUserResolveService,
$WebfingerService,
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index edb9335b6e..7e11b9cdca 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -20,7 +20,7 @@ import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
-const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
+const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {
@@ -68,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
}, moderator?: MiUser): Promise<MiEmoji> {
- const emoji = await this.emojisRepository.insert({
+ const emoji = await this.emojisRepository.insertOne({
id: this.idService.gen(),
updatedAt: new Date(),
name: data.name,
@@ -82,7 +82,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive: data.isSensitive,
localOnly: data.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
- }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+ });
if (data.host == null) {
this.localEmojisCache.refresh();
@@ -346,10 +346,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
- const res = {} as any;
+ const res = {} as Record<string, string>;
for (let i = 0; i < emojiNames.length; i++) {
- if (emojis[i] != null) {
- res[emojiNames[i]] = emojis[i];
+ const resolvedEmoji = emojis[i];
+ if (resolvedEmoji != null) {
+ res[emojiNames[i]] = resolvedEmoji;
}
}
return res;
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index 1bc1df1dda..37c5d1adf7 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -220,7 +220,7 @@ export class DriveService {
file.size = size;
file.storedInternal = false;
- return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ return await this.driveFilesRepository.insertOne(file);
} else { // use internal storage
const accessKey = randomUUID();
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
@@ -254,7 +254,7 @@ export class DriveService {
file.md5 = hash;
file.size = size;
- return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ return await this.driveFilesRepository.insertOne(file);
}
}
@@ -497,14 +497,20 @@ export class DriveService {
if (user && !force) {
// Check if there is a file with the same hash
- const much = await this.driveFilesRepository.findOneBy({
+ const matched = 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;
+ if (matched) {
+ this.registerLogger.info(`file with same hash is found: ${matched.id}`);
+ if (sensitive && !matched.isSensitive) {
+ // The file is federated as sensitive for this time, but was federated as non-sensitive before.
+ // Therefore, update the file to sensitive.
+ await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true });
+ matched.isSensitive = true;
+ }
+ return matched;
}
}
@@ -609,7 +615,7 @@ export class DriveService {
file.type = info.type.mime;
file.storedInternal = false;
- file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ file = await this.driveFilesRepository.insertOne(file);
} catch (err) {
// duplicate key error (when already registered)
if (isDuplicateKeyValueError(err)) {
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index 9c239b4dfc..d5058f37c2 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -13,7 +13,7 @@ import type { NotesRepository } from '@/models/_.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import { CacheService } from '@/core/CacheService.js';
import { isReply } from '@/misc/is-reply.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
@@ -61,8 +61,8 @@ export class FanoutTimelineEndpointService {
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
- const shouldPrepend = ps.sinceId && !ps.untilId;
- const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
+ const ascending = ps.sinceId && !ps.untilId;
+ const idCompare: (a: string, b: string) => number = ascending ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
@@ -95,7 +95,7 @@ export class FanoutTimelineEndpointService {
if (ps.excludePureRenotes) {
const parentFilter = filter;
- filter = (note) => !isPureRenote(note) && parentFilter(note);
+ filter = (note) => (!isRenote(note) || isQuote(note)) && parentFilter(note);
}
if (ps.me) {
@@ -116,7 +116,7 @@ export class FanoutTimelineEndpointService {
filter = (note) => {
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
- if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false;
+ if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
if (isInstanceMuted(note, userMutedInstances)) return false;
return parentFilter(note);
@@ -142,9 +142,7 @@ export class FanoutTimelineEndpointService {
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
// 十分Redisからとれた
- const result = redisTimeline.slice(0, ps.limit);
- if (shouldPrepend) result.reverse();
- return result;
+ return redisTimeline.slice(0, ps.limit);
}
}
@@ -152,8 +150,7 @@ export class FanoutTimelineEndpointService {
const remainingToRead = ps.limit - redisTimeline.length;
let dbUntil: string | null;
let dbSince: string | null;
- if (shouldPrepend) {
- redisTimeline.reverse();
+ if (ascending) {
dbUntil = ps.untilId;
dbSince = noteIds[noteIds.length - 1];
} else {
@@ -161,7 +158,7 @@ export class FanoutTimelineEndpointService {
dbSince = ps.sinceId;
}
const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead);
- return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb];
+ return [...redisTimeline, ...gotFromDb];
}
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 66db2067d9..6799f2c5bb 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -55,11 +55,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
const index = await this.instancesRepository.findOneBy({ host });
if (index == null) {
- const i = await this.instancesRepository.insert({
+ const i = await this.instancesRepository.insertOne({
id: this.idService.gen(),
host,
firstRetrievedAt: new Date(),
- }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.federatedInstanceCache.set(host, i);
return i;
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index bc270bd28f..aa16468ecb 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -51,21 +51,35 @@ export class FetchInstanceMetadataService {
}
@bindThis
- public async tryLock(host: string): Promise<boolean> {
- const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
- return mutex !== '1';
+ // public for test
+ public async tryLock(host: string): Promise<string | null> {
+ // TODO: マイグレーションなのであとで消す (2024.3.1)
+ this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
+
+ return await this.redisClient.set(
+ `fetchInstanceMetadata:mutex:v2:${host}`, '1',
+ 'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
+ 'GET' // 古い値を返す(なかったらnull)
+ );
}
@bindThis
- public unlock(host: string): Promise<'OK'> {
- return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
+ // public for test
+ public unlock(host: string): Promise<number> {
+ return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`);
}
@bindThis
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
const host = instance.host;
- // Acquire mutex to ensure no parallel runs
- if (!await this.tryLock(host)) return;
+
+ // finallyでunlockされてしまうのでtry内でロックチェックをしない
+ // (returnであってもfinallyは実行される)
+ if (!force && await this.tryLock(host) === '1') {
+ // 1が返ってきていたらロックされているという意味なので、何もしない
+ return;
+ }
+
try {
if (!force) {
const _instance = await this.federatedInstanceService.fetch(host);
@@ -140,7 +154,7 @@ export class FetchInstanceMetadataService {
throw new Error('No wellknown links');
}
- const links = wellknown.links as any[];
+ const links = wellknown.links as ({ rel: string, href: string; })[];
const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts
index b8babcb3a7..169285f033 100644
--- a/packages/backend/src/core/FileInfoService.ts
+++ b/packages/backend/src/core/FileInfoService.ts
@@ -14,11 +14,12 @@ 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 { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import { encode } from 'blurhash';
import { createTempDir } from '@/misc/create-temp.js';
import { AiService } from '@/core/AiService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
export type FileInfo = {
@@ -49,9 +50,13 @@ const TYPE_SVG = {
@Injectable()
export class FileInfoService {
+ private logger: Logger;
+
constructor(
private aiService: AiService,
+ private loggerService: LoggerService,
) {
+ this.logger = this.loggerService.getLogger('file-info');
}
/**
@@ -318,6 +323,34 @@ export class FileInfoService {
}
/**
+ * ビデオファイルにビデオトラックがあるかどうかチェック
+ * (ない場合:m4a, webmなど)
+ *
+ * @param path ファイルパス
+ * @returns ビデオトラックがあるかどうか(エラー発生時は常に`true`を返す)
+ */
+ @bindThis
+ private hasVideoTrackOnVideoFile(path: string): Promise<boolean> {
+ const sublogger = this.logger.createSubLogger('ffprobe');
+ sublogger.info(`Checking the video file. File path: ${path}`);
+ return new Promise((resolve) => {
+ try {
+ FFmpeg.ffprobe(path, (err, metadata) => {
+ if (err) {
+ sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err);
+ resolve(true);
+ return;
+ }
+ resolve(metadata.streams.some((stream) => stream.codec_type === 'video'));
+ });
+ } catch (err) {
+ sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error);
+ resolve(true);
+ }
+ });
+ }
+
+ /**
* Detect MIME Type and extension
*/
@bindThis
@@ -339,6 +372,20 @@ export class FileInfoService {
return TYPE_SVG;
}
+ if ((type.mime.startsWith('video') || type.mime === 'application/ogg') && !(await this.hasVideoTrackOnVideoFile(path))) {
+ const newMime = `audio/${type.mime.split('/')[1]}`;
+ if (newMime === 'audio/mp4') {
+ return {
+ mime: 'audio/mp4',
+ ext: 'm4a',
+ };
+ }
+ return {
+ mime: newMime,
+ ext: type.ext,
+ };
+ }
+
return {
mime: this.fixMime(type.mime),
ext: type.ext,
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index c62ee5a642..9786f8b8bb 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -6,10 +6,11 @@
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import * as parse5 from 'parse5';
-import { Window } from 'happy-dom';
+import { Window, XMLSerializer } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { intersperse } from '@/misc/prelude/array.js';
+import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
@@ -33,6 +34,8 @@ export class MfmService {
// some AP servers like Pixelfed use br tags as well as newlines
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
+ const normalizedHashtagNames = hashtagNames == null ? undefined : new Set<string>(hashtagNames.map(x => normalizeForSearch(x)));
+
const dom = parse5.parseFragment(html);
let text = '';
@@ -85,7 +88,7 @@ export class MfmService {
const href = node.attrs.find(x => x.name === 'href');
// ハッシュタグ
- if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
+ if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
text += txt;
// メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
@@ -244,6 +247,8 @@ export class MfmService {
const doc = window.document;
+ const body = doc.createElement('p');
+
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);
@@ -454,8 +459,8 @@ export class MfmService {
},
};
- appendChildren(nodes, doc.body);
+ appendChildren(nodes, body);
- return `<p>${doc.body.innerHTML}</p>`;
+ return new XMLSerializer().serializeToString(body);
}
}
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 81ae2908d3..e5580f36d1 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -306,7 +306,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// Check blocking
- if (data.renote && !this.isQuote(data)) {
+ if (this.isRenote(data) && !this.isQuote(data)) {
if (data.renote.userHost === null) {
if (data.renote.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
@@ -473,6 +473,7 @@ export class NoteCreateService implements OnApplicationShutdown {
noteVisibility: insert.visibility,
userId: user.id,
userHost: user.host,
+ channelId: insert.channelId,
});
await transactionalEntityManager.insert(MiPoll, poll);
@@ -641,7 +642,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// If it is renote
- if (data.renote) {
+ if (this.isRenote(data)) {
const type = this.isQuote(data) ? 'quote' : 'renote';
// Notify
@@ -725,9 +726,20 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- private isQuote(note: Option): note is Option & { renote: MiNote } {
- // sync with misc/is-quote.ts
- return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll);
+ private isRenote(note: Option): note is Option & { renote: MiNote } {
+ return note.renote != null;
+ }
+
+ @bindThis
+ private isQuote(note: Option & { renote: MiNote }): note is Option & { renote: MiNote } & (
+ { text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
+ ) {
+ // NOTE: SYNC WITH misc/is-quote.ts
+ return note.text != null ||
+ note.reply != null ||
+ note.cw != null ||
+ note.poll != null ||
+ (note.files != null && note.files.length > 0);
}
@bindThis
@@ -795,7 +807,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
if (data.localOnly) return null;
- const content = data.renote && !this.isQuote(data)
+ const content = this.isRenote(data) && !this.isQuote(data)
? 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);
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index fdf843c3e8..801ed02e00 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -24,7 +24,7 @@ import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
@Injectable()
export class NoteDeleteService {
@@ -79,7 +79,7 @@ export class NoteDeleteService {
let renote: MiNote | null = null;
// if deleted note is renote
- if (isPureRenote(note)) {
+ if (isRenote(note) && !isQuote(note)) {
renote = await this.notesRepository.findOneBy({
id: note.renoteId,
});
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index 3b706d9854..6a845b951d 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -101,7 +101,7 @@ export class PushNotificationService implements OnApplicationShutdown {
type,
body: (type === 'notification' || type === 'unreadAntennaNote') ? truncateBody(type, body) : body,
userId,
- dateTime: (new Date()).getTime(),
+ dateTime: Date.now(),
}), {
proxy: this.config.proxy,
}).catch((err: any) => {
diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index e9dc9b57af..8dd3d64f5b 100644
--- a/packages/backend/src/core/RelayService.ts
+++ b/packages/backend/src/core/RelayService.ts
@@ -53,11 +53,11 @@ export class RelayService {
@bindThis
public async addRelay(inbox: string): Promise<MiRelay> {
- const relay = await this.relaysRepository.insert({
+ const relay = await this.relaysRepository.insertOne({
id: this.idService.gen(),
inbox,
status: 'requesting',
- }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
+ });
const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 439bc08845..7f939b99c7 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -281,7 +281,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
- const game = await this.reversiGamesRepository.insert({
+ const game = await this.reversiGamesRepository.insertOne({
id: this.idService.gen(),
user1Id: parentId,
user2Id: childId,
@@ -294,10 +294,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw: 'random',
isLlotheo: false,
noIrregularRules: options.noIrregularRules,
- }).then(x => this.reversiGamesRepository.findOneOrFail({
- where: { id: x.identifiers[0].id },
- relations: ['user1', 'user2'],
- }));
+ }, { relations: ['user1', 'user2'] });
this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game);
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 09f3097114..d6eea70297 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -205,45 +205,79 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
try {
switch (value.type) {
+ // ~かつ~
case 'and': {
return value.values.every(v => this.evalCond(user, roles, v));
}
+ // ~または~
case 'or': {
return value.values.some(v => this.evalCond(user, roles, v));
}
+ // ~ではない
case 'not': {
return !this.evalCond(user, roles, value.value);
}
+ // マニュアルロールがアサインされている
case 'roleAssignedTo': {
return roles.some(r => r.id === value.roleId);
}
+ // ローカルユーザのみ
case 'isLocal': {
return this.userEntityService.isLocalUser(user);
}
+ // リモートユーザのみ
case 'isRemote': {
return this.userEntityService.isRemoteUser(user);
}
+ // サスペンド済みユーザである
+ case 'isSuspended': {
+ return user.isSuspended;
+ }
+ // 鍵アカウントユーザである
+ case 'isLocked': {
+ return user.isLocked;
+ }
+ // botユーザである
+ case 'isBot': {
+ return user.isBot;
+ }
+ // 猫である
+ case 'isCat': {
+ return user.isCat;
+ }
+ // 「ユーザを見つけやすくする」が有効なアカウント
+ case 'isExplorable': {
+ return user.isExplorable;
+ }
+ // ユーザが作成されてから指定期間経過した
case 'createdLessThan': {
return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000));
}
+ // ユーザが作成されてから指定期間経っていない
case 'createdMoreThan': {
return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000));
}
+ // フォロワー数が指定値以下
case 'followersLessThanOrEq': {
return user.followersCount <= value.value;
}
+ // フォロワー数が指定値以上
case 'followersMoreThanOrEq': {
return user.followersCount >= value.value;
}
+ // フォロー数が指定値以下
case 'followingLessThanOrEq': {
return user.followingCount <= value.value;
}
+ // フォロー数が指定値以上
case 'followingMoreThanOrEq': {
return user.followingCount >= value.value;
}
+ // ノート数が指定値以下
case 'notesLessThanOrEq': {
return user.notesCount <= value.value;
}
+ // ノート数が指定値以上
case 'notesMoreThanOrEq': {
return user.notesCount >= value.value;
}
@@ -437,12 +471,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
}
- const created = await this.roleAssignmentsRepository.insert({
+ const created = await this.roleAssignmentsRepository.insertOne({
id: this.idService.gen(now),
expiresAt: expiresAt,
roleId: roleId,
userId: userId,
- }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.rolesRepository.update(roleId, {
lastUsedAt: new Date(),
@@ -524,7 +558,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
const date = new Date();
- const created = await this.rolesRepository.insert({
+ const created = await this.rolesRepository.insertOne({
id: this.idService.gen(date.getTime()),
updatedAt: date,
lastUsedAt: date,
@@ -542,7 +576,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
- }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.globalEventService.publishInternalEvent('roleCreated', created);
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 0a492c06e4..406ea04031 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -511,7 +511,13 @@ export class UserFollowingService implements OnModuleInit {
if (blocking) throw new Error('blocking');
if (blocked) throw new Error('blocked');
- const followRequest = await this.followRequestsRepository.insert({
+ // Remove old follow requests before creating a new one.
+ await this.followRequestsRepository.delete({
+ followeeId: followee.id,
+ followerId: follower.id,
+ });
+
+ const followRequest = await this.followRequestsRepository.insertOne({
id: this.idService.gen(),
followerId: follower.id,
followeeId: followee.id,
@@ -525,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
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)) {
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index 42fbed2110..ec9f4484a4 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -10,7 +10,7 @@ import {
generateRegistrationOptions, verifyAuthenticationResponse,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
-import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers';
+import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
import { DI } from '@/di-symbols.js';
import type { UserSecurityKeysRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
@@ -49,7 +49,7 @@ export class WebAuthnService {
const instance = await this.metaService.fetch();
return {
origin: this.config.url,
- rpId: this.config.host,
+ rpId: this.config.hostname,
rpName: instance.name ?? this.config.host,
rpIcon: instance.iconUrl ?? undefined,
};
@@ -65,13 +65,12 @@ export class WebAuthnService {
const registrationOptions = await generateRegistrationOptions({
rpName: relyingParty.rpName,
rpID: relyingParty.rpId,
- userID: userId,
+ userID: isoUint8Array.fromUTF8String(userId),
userName: userName,
userDisplayName: userDisplayName,
attestationType: 'indirect',
- excludeCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
- id: Buffer.from(key.id, 'base64url'),
- type: 'public-key',
+ excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
+ id: key.id,
transports: key.transports ?? undefined,
})),
authenticatorSelection: {
@@ -87,7 +86,7 @@ export class WebAuthnService {
@bindThis
public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{
- credentialID: Uint8Array;
+ credentialID: string;
credentialPublicKey: Uint8Array;
attestationObject: Uint8Array;
fmt: AttestationFormat;
@@ -144,6 +143,7 @@ export class WebAuthnService {
@bindThis
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
+ const relyingParty = await this.getRelyingParty();
const keys = await this.userSecurityKeysRepository.findBy({
userId: userId,
});
@@ -153,9 +153,9 @@ export class WebAuthnService {
}
const authenticationOptions = await generateAuthenticationOptions({
- allowCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
- id: Buffer.from(key.id, 'base64url'),
- type: 'public-key',
+ rpID: relyingParty.rpId,
+ allowCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
+ id: key.id,
transports: key.transports ?? undefined,
})),
userVerification: 'preferred',
@@ -219,7 +219,7 @@ export class WebAuthnService {
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
authenticator: {
- credentialID: Buffer.from(key.id, 'base64url'),
+ credentialID: key.id,
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 1621c41bcc..d0d206760c 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -28,6 +28,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserR
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -36,9 +37,8 @@ import { ApResolverService } from './ApResolverService.js';
import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Resolver } from './ApResolverService.js';
-import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
+import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
@Injectable()
export class ApInboxService {
@@ -90,13 +90,15 @@ export class ApInboxService {
}
@bindThis
- public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
+ public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
+ let result = undefined as string | void;
if (isCollectionOrOrderedCollection(activity)) {
+ const results = [] as [string, string | void][];
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);
+ results.push([getApId(item), await this.performOneActivity(actor, act)]);
} catch (err) {
if (err instanceof Error || typeof err === 'string') {
this.logger.error(err);
@@ -105,8 +107,13 @@ export class ApInboxService {
}
}
}
+
+ const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok')));
+ if (hasReason) {
+ result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
+ }
} else {
- await this.performOneActivity(actor, activity);
+ result = await this.performOneActivity(actor, activity);
}
// ついでにリモートユーザーの情報が古かったら更新しておく
@@ -117,42 +124,43 @@ export class ApInboxService {
});
}
}
+ return result;
}
@bindThis
- public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
+ public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
if (actor.isSuspended) return;
if (isCreate(activity)) {
- await this.create(actor, activity);
+ return await this.create(actor, activity);
} else if (isDelete(activity)) {
- await this.delete(actor, activity);
+ return await this.delete(actor, activity);
} else if (isUpdate(activity)) {
- await this.update(actor, activity);
+ return await this.update(actor, activity);
} else if (isFollow(activity)) {
- await this.follow(actor, activity);
+ return await this.follow(actor, activity);
} else if (isAccept(activity)) {
- await this.accept(actor, activity);
+ return await this.accept(actor, activity);
} else if (isReject(activity)) {
- await this.reject(actor, activity);
+ return await this.reject(actor, activity);
} else if (isAdd(activity)) {
- await this.add(actor, activity).catch(err => this.logger.error(err));
+ return await this.add(actor, activity);
} else if (isRemove(activity)) {
- await this.remove(actor, activity).catch(err => this.logger.error(err));
+ return await this.remove(actor, activity);
} else if (isAnnounce(activity)) {
- await this.announce(actor, activity);
+ return await this.announce(actor, activity);
} else if (isLike(activity)) {
- await this.like(actor, activity);
+ return await this.like(actor, activity);
} else if (isUndo(activity)) {
- await this.undo(actor, activity);
+ return await this.undo(actor, activity);
} else if (isBlock(activity)) {
- await this.block(actor, activity);
+ return await this.block(actor, activity);
} else if (isFlag(activity)) {
- await this.flag(actor, activity);
+ return await this.flag(actor, activity);
} else if (isMove(activity)) {
- await this.move(actor, activity);
+ return await this.move(actor, activity);
} else {
- this.logger.warn(`unrecognized activity type: ${activity.type}`);
+ return `unrecognized activity type: ${activity.type}`;
}
}
@@ -234,38 +242,49 @@ export class ApInboxService {
}
@bindThis
- private async add(actor: MiRemoteUser, activity: IAdd): Promise<void> {
+ private async add(actor: MiRemoteUser, activity: IAdd): Promise<string | void> {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
if (activity.target == null) {
- throw new Error('target is null');
+ return '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');
+ if (note == null) return 'note not found';
await this.notePiningService.addPinned(actor, note.id);
return;
}
- throw new Error(`unknown target: ${activity.target}`);
+ return `unknown target: ${activity.target}`;
}
@bindThis
- private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<void> {
+ private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<string | void> {
const uri = getApId(activity);
this.logger.info(`Announce: ${uri}`);
+ const resolver = this.apResolverService.createResolver();
+
+ if (!activity.object) return 'skip: activity has no object property';
const targetUri = getApId(activity.object);
+ if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
+
+ const target = await resolver.resolve(activity.object).catch(e => {
+ this.logger.error(`Resolution failed: ${e}`);
+ return e;
+ });
+
+ if (isPost(target)) return await this.announceNote(actor, activity, target);
- await this.announceNote(actor, activity, targetUri);
+ return `skip: unknown object type ${getApType(target)}`;
}
@bindThis
- private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
+ private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise<string | void> {
const uri = getApId(activity);
if (actor.isSuspended) {
@@ -288,24 +307,21 @@ export class ApInboxService {
// Announce対象をresolve
let renote;
try {
- renote = await this.apNoteService.resolveNote(targetUri);
- if (renote == null) throw new Error('announce target is null');
+ renote = await this.apNoteService.resolveNote(target);
+ if (renote == null) return 'announce target is null';
} catch (err) {
// 対象が4xxならスキップ
if (err instanceof StatusError) {
if (!err.isRetryable) {
- this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
- return;
+ return `Ignored announce target ${target.id} - ${err.statusCode}`;
}
-
- this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
+ return `Error in announce target ${target.id} - ${err.statusCode}`;
}
throw err;
}
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
- this.logger.warn('skip: invalid actor for this activity');
- return;
+ return 'skip: invalid actor for this activity';
}
this.logger.info(`Creating the (Re)Note: ${uri}`);
@@ -314,8 +330,7 @@ export class ApInboxService {
const createdAt = activity.published ? new Date(activity.published) : null;
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
- this.logger.warn('skip: malformed createdAt');
- return;
+ return 'skip: malformed createdAt';
}
await this.noteCreateService.create(actor, {
@@ -349,11 +364,15 @@ export class ApInboxService {
}
@bindThis
- private async create(actor: MiRemoteUser, activity: ICreate): Promise<void> {
+ private async create(actor: MiRemoteUser, activity: ICreate): Promise<string | void> {
const uri = getApId(activity);
this.logger.info(`Create: ${uri}`);
+ if (!activity.object) return 'skip: activity has no object property';
+ const targetUri = getApId(activity.object);
+ if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
+
// copy audiences between activity <=> object.
if (typeof activity.object === 'object') {
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
@@ -380,7 +399,7 @@ export class ApInboxService {
if (isPost(object)) {
await this.createNote(resolver, actor, object, false, activity);
} else {
- this.logger.warn(`Unknown type: ${getApType(object)}`);
+ return `Unknown type: ${getApType(object)}`;
}
}
@@ -422,7 +441,7 @@ export class ApInboxService {
@bindThis
private async delete(actor: MiRemoteUser, activity: IDelete): Promise<string> {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
// 削除対象objectのtype
@@ -581,29 +600,29 @@ export class ApInboxService {
}
@bindThis
- private async remove(actor: MiRemoteUser, activity: IRemove): Promise<void> {
+ private async remove(actor: MiRemoteUser, activity: IRemove): Promise<string | void> {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
if (activity.target == null) {
- throw new Error('target is null');
+ return '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');
+ if (note == null) return 'note not found';
await this.notePiningService.removePinned(actor, note.id);
return;
}
- throw new Error(`unknown target: ${activity.target}`);
+ return `unknown target: ${activity.target}`;
}
@bindThis
private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
const uri = activity.id ?? activity;
@@ -614,7 +633,7 @@ export class ApInboxService {
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
- throw e;
+ return e;
});
// don't queue because the sender may attempt again when timeout
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index d7fb977a99..4fc724b548 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -28,8 +28,9 @@ import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
-import { LdSignatureService } from './LdSignatureService.js';
+import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
+import { CONTEXT } from './misc/contexts.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
@Injectable()
@@ -56,7 +57,7 @@ export class ApRendererService {
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
- private ldSignatureService: LdSignatureService,
+ private jsonLdService: JsonLdService,
private userKeypairService: UserKeypairService,
private apMfmService: ApMfmService,
private mfmService: MfmService,
@@ -166,6 +167,7 @@ export class ApRendererService {
mediaType: file.webpublicType ?? file.type,
url: this.driveFileEntityService.getPublicUrl(file),
name: file.comment,
+ sensitive: file.isSensitive,
};
}
@@ -617,48 +619,16 @@ export class ApRendererService {
x.id = `${this.config.url}/${randomUUID()}`;
}
- return Object.assign({
- '@context': [
- 'https://www.w3.org/ns/activitystreams',
- 'https://w3id.org/security/v1',
- {
- Key: 'sec:Key',
- // 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_summary': 'misskey:_misskey_summary',
- 'isCat': 'misskey:isCat',
- // vcard
- vcard: 'http://www.w3.org/2006/vcard/ns#',
- },
- ],
- }, x as T & { id: string });
+ return Object.assign({ '@context': CONTEXT }, x as T & { id: string });
}
@bindThis
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
const keypair = await this.userKeypairService.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`);
+ const jsonLd = this.jsonLdService.use();
+ jsonLd.debug = false;
+ activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
return activity;
}
diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/JsonLdService.ts
index 9de184336f..100d4fa19f 100644
--- a/packages/backend/src/core/activitypub/LdSignatureService.ts
+++ b/packages/backend/src/core/activitypub/JsonLdService.ts
@@ -7,14 +7,14 @@ import * as crypto from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
-import { CONTEXTS } from './misc/contexts.js';
+import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
import type { JsonLdDocument } from 'jsonld';
-import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
+import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js';
-// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
+// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
-class LdSignature {
+class JsonLd {
public debug = false;
public preLoad = true;
public loderTimeout = 5000;
@@ -89,10 +89,18 @@ class LdSignature {
}
@bindThis
- public async normalize(data: JsonLdDocument): Promise<string> {
+ public async compact(data: any, context: any = CONTEXT): Promise<JsonLdDocument> {
const customLoader = this.getLoader();
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
+ return (await import('jsonld')).default.compact(data, context, {
+ documentLoader: customLoader,
+ });
+ }
+
+ @bindThis
+ public async normalize(data: JsonLdDocument): Promise<string> {
+ const customLoader = this.getLoader();
return (await import('jsonld')).default.normalize(data, {
documentLoader: customLoader,
});
@@ -104,11 +112,11 @@ class LdSignature {
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
if (this.preLoad) {
- if (url in CONTEXTS) {
+ if (url in PRELOADED_CONTEXTS) {
if (this.debug) console.debug(`HIT: ${url}`);
return {
contextUrl: undefined,
- document: CONTEXTS[url],
+ document: PRELOADED_CONTEXTS[url],
documentUrl: url,
};
}
@@ -125,7 +133,7 @@ class LdSignature {
}
@bindThis
- private async fetchDocument(url: string): Promise<JsonLd> {
+ private async fetchDocument(url: string): Promise<JsonLdObject> {
const json = await this.httpRequestService.send(
url,
{
@@ -146,7 +154,7 @@ class LdSignature {
}
});
- return json as JsonLd;
+ return json as JsonLdObject;
}
@bindThis
@@ -158,14 +166,14 @@ class LdSignature {
}
@Injectable()
-export class LdSignatureService {
+export class JsonLdService {
constructor(
private httpRequestService: HttpRequestService,
) {
}
@bindThis
- public use(): LdSignature {
- return new LdSignature(this.httpRequestService);
+ public use(): JsonLd {
+ return new JsonLd(this.httpRequestService);
}
}
diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts
index 88afdefcd3..feb8c42c56 100644
--- a/packages/backend/src/core/activitypub/misc/contexts.ts
+++ b/packages/backend/src/core/activitypub/misc/contexts.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import type { JsonLd } from 'jsonld/jsonld-spec.js';
+import type { Context, JsonLd } from 'jsonld/jsonld-spec.js';
/* eslint:disable:quotemark indent */
const id_v1 = {
@@ -526,7 +526,42 @@ const activitystreams = {
},
} satisfies JsonLd;
-export const CONTEXTS: Record<string, JsonLd> = {
+const context_iris = [
+ 'https://www.w3.org/ns/activitystreams',
+ 'https://w3id.org/security/v1',
+];
+
+const extension_context_definition = {
+ Key: 'sec:Key',
+ // 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_summary': 'misskey:_misskey_summary',
+ 'isCat': 'misskey:isCat',
+ // vcard
+ vcard: 'http://www.w3.org/2006/vcard/ns#',
+} satisfies Context;
+
+export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition];
+
+export const PRELOADED_CONTEXTS: Record<string, JsonLd> = {
'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/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index 89b6ef23d0..3691967270 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApLoggerService } from '../ApLoggerService.js';
-import type { IObject } from '../type.js';
+import { isDocument, type IObject } from '../type.js';
@Injectable()
export class ApImageService {
@@ -39,7 +39,7 @@ export class ApImageService {
* Imageを作成します。
*/
@bindThis
- public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
+ public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
@@ -47,16 +47,18 @@ export class ApImageService {
const image = await this.apResolverService.createResolver().resolve(value);
+ if (!isDocument(image)) return null;
+
if (image.url == null) {
- throw new Error('invalid image: url not provided');
+ return null;
}
if (typeof image.url !== 'string') {
- throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
+ return null;
}
if (!checkHttps(image.url)) {
- throw new Error('invalid image: unexpected schema of url: ' + image.url);
+ return null;
}
this.logger.info(`Creating the Image: ${image.url}`);
@@ -86,12 +88,11 @@ export class ApImageService {
/**
* Imageを解決します。
*
- * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ * ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
@bindThis
- public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
- // TODO
+ public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
+ // TODO: Misskeyに対象のImageが登録されていればそれを返す
// リモートサーバーからフェッチしてきて登録
return await this.createImage(actor, value);
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index b2fd435f93..c6e6b3a1e8 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -4,7 +4,6 @@
*/
import { forwardRef, Inject, Injectable } from '@nestjs/common';
-import promiseLimit from 'promise-limit';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
@@ -82,20 +81,20 @@ export class ApNoteService {
const expectHost = this.utilityService.extractDbHost(uri);
if (!validPost.includes(getApType(object))) {
- return new Error(`invalid Note: invalid object type ${getApType(object)}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `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)}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
}
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
if (object.attributedTo && actualHost !== expectHost) {
- return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
}
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
- return new Error('invalid Note: published timestamp is malformed');
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
}
return null;
@@ -129,7 +128,7 @@ export class ApNoteService {
value,
object,
});
- throw new Error('invalid note');
+ throw err;
}
const note = object as IPost;
@@ -209,15 +208,13 @@ export class ApNoteService {
}
// 添付ファイル
- // TODO: attachmentは必ずしもImageではない
- // TODO: attachmentは必ずしも配列ではない
- const limit = promiseLimit<MiDriveFile>(2);
- const files = (await Promise.all(toArray(note.attachment).map(attach => (
- limit(() => this.apImageService.resolveImage(actor, {
- ...attach,
- sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
- }))
- ))));
+ const files: MiDriveFile[] = [];
+
+ for (const attach of toArray(note.attachment)) {
+ attach.sensitive ??= note.sensitive;
+ const file = await this.apImageService.resolveImage(actor, attach);
+ if (file) files.push(file);
+ }
// リプライ
const reply: MiNote | null = note.inReplyTo
@@ -410,7 +407,7 @@ export class ApNoteService {
this.logger.info(`register emoji host=${host}, name=${name}`);
- return await this.emojisRepository.insert({
+ return await this.emojisRepository.insertOne({
id: this.idService.gen(),
host,
name,
@@ -419,7 +416,7 @@ export class ApNoteService {
publicUrl: tag.icon.url,
updatedAt: new Date(),
aliases: [],
- }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+ });
}));
}
}
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index b43dddad61..5b6c6c8ca6 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -25,6 +25,7 @@ export interface IObject {
endTime?: Date;
icon?: any;
image?: any;
+ mediaType?: string;
url?: ApObject | string;
href?: string;
tag?: IObject | IObject[];
@@ -240,14 +241,14 @@ export interface IKey extends IObject {
}
export interface IApDocument extends IObject {
- type: 'Document';
- name: string | null;
- mediaType: string;
+ type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
}
-export interface IApImage extends IObject {
+export const isDocument = (object: IObject): object is IApDocument =>
+ ['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
+
+export interface IApImage extends IApDocument {
type: 'Image';
- name: string | null;
}
export interface ICreate extends IActivity {
@@ -327,3 +328,4 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
+export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts
index aa0cb9dc2b..af5485a46e 100644
--- a/packages/backend/src/core/chart/core.ts
+++ b/packages/backend/src/core/chart/core.ts
@@ -14,7 +14,8 @@ import { EntitySchema, LessThan, Between } from 'typeorm';
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
-import type { Repository, DataSource } from 'typeorm';
+import { MiRepository, miRepository } from '@/models/_.js';
+import type { DataSource, Repository } from 'typeorm';
const COLUMN_PREFIX = '___' as const;
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
@@ -145,10 +146,10 @@ export default abstract class Chart<T extends Schema> {
group: string | null;
}[] = [];
// ↓にしたいけどfindOneとかで型エラーになる
- //private repositoryForHour: Repository<RawRecord<T>>;
- //private repositoryForDay: Repository<RawRecord<T>>;
- private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>;
- private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>;
+ //private repositoryForHour: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
+ //private repositoryForDay: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
+ private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
+ private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
/**
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
@@ -211,6 +212,10 @@ export default abstract class Chart<T extends Schema> {
} {
const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({
name:
+ span === 'hour' ? `ChartX${name}` :
+ span === 'day' ? `ChartDayX${name}` :
+ new Error('not happen') as never,
+ tableName:
span === 'hour' ? `__chart__${camelToSnake(name)}` :
span === 'day' ? `__chart_day__${camelToSnake(name)}` :
new Error('not happen') as never,
@@ -271,8 +276,8 @@ export default abstract class Chart<T extends Schema> {
this.logger = logger;
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);
+ this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
+ this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
}
@bindThis
@@ -387,11 +392,11 @@ export default abstract class Chart<T extends Schema> {
}
// 新規ログ挿入
- log = await repository.insert({
+ log = await repository.insertOne({
date: date,
...(group ? { group: group } : {}),
...columns,
- }).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord<T>;
+ }) as RawRecord<T>;
this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
@@ -459,13 +464,15 @@ export default abstract class Chart<T extends Schema> {
}
}
- // bake unique count
+ // bake cardinality
for (const [k, v] of Object.entries(finalDiffs)) {
if (this.schema[k].uniqueIncrement) {
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
- 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;
+ const cardinalityOfHour = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size;
+ const cardinalityOfDay = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size;
+ queryForHour[name] = cardinalityOfHour;
+ queryForDay[name] = cardinalityOfDay;
}
}
@@ -637,7 +644,7 @@ export default abstract class Chart<T extends Schema> {
// 要求された範囲にログがひとつもなかったら
if (logs.length === 0) {
// もっとも新しいログを持ってくる
- // (すくなくともひとつログが無いと隙間埋めできないため)
+ // (すくなくともひとつログが無いと補間できないため)
const recentLog = await repository.findOne({
where: group ? {
group: group,
@@ -654,7 +661,7 @@ export default abstract class Chart<T extends Schema> {
// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
} else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) {
// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
- // (隙間埋めできないため)
+ // (補間できないため)
const outdatedLog = await repository.findOne({
where: {
date: LessThan(Chart.dateToTimestamp(gt)),
@@ -683,7 +690,7 @@ export default abstract class Chart<T extends Schema> {
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));
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index 49f256d870..b0e1d1ab36 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -10,6 +10,8 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -26,6 +28,11 @@ export class AbuseUserReportEntityService {
@bindThis
public async pack(
src: MiAbuseUserReport['id'] | MiAbuseUserReport,
+ hint?: {
+ packedReporter?: Packed<'UserDetailedNotMe'>,
+ packedTargetUser?: Packed<'UserDetailedNotMe'>,
+ packedAssignee?: Packed<'UserDetailedNotMe'>,
+ },
) {
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
@@ -37,13 +44,13 @@ export class AbuseUserReportEntityService {
reporterId: report.reporterId,
targetUserId: report.targetUserId,
assigneeId: report.assigneeId,
- reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
+ reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
schema: 'UserDetailedNotMe',
}),
- targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
+ targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
schema: 'UserDetailedNotMe',
}),
- assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
+ assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
@@ -51,9 +58,24 @@ export class AbuseUserReportEntityService {
}
@bindThis
- public packMany(
- reports: any[],
+ public async packMany(
+ reports: MiAbuseUserReport[],
) {
- return Promise.all(reports.map(x => this.pack(x)));
+ const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
+ const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
+ const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull);
+ const _userMap = await this.userEntityService.packMany(
+ [..._reporters, ..._targetUsers, ..._assignees],
+ null,
+ { schema: 'UserDetailedNotMe' },
+ ).then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ reports.map(report => {
+ const packedReporter = _userMap.get(report.reporterId);
+ const packedTargetUser = _userMap.get(report.targetUserId);
+ const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined;
+ return this.pack(report, { packedReporter, packedTargetUser, packedAssignee });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/AnnouncementEntityService.ts b/packages/backend/src/core/entities/AnnouncementEntityService.ts
new file mode 100644
index 0000000000..90b04d0229
--- /dev/null
+++ b/packages/backend/src/core/entities/AnnouncementEntityService.ts
@@ -0,0 +1,71 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import type { AnnouncementsRepository, AnnouncementReadsRepository, MiAnnouncement, MiUser } from '@/models/_.js';
+import type { Packed } from '@/misc/json-schema.js';
+import { bindThis } from '@/decorators.js';
+import { IdService } from '@/core/IdService.js';
+
+@Injectable()
+export class AnnouncementEntityService {
+ constructor(
+ @Inject(DI.announcementsRepository)
+ private announcementsRepository: AnnouncementsRepository,
+
+ @Inject(DI.announcementReadsRepository)
+ private announcementReadsRepository: AnnouncementReadsRepository,
+
+ private idService: IdService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null },
+ me?: { id: MiUser['id'] } | null | undefined,
+ ): Promise<Packed<'Announcement'>> {
+ const announcement = typeof src === 'object'
+ ? src
+ : await this.announcementsRepository.findOneByOrFail({
+ id: src,
+ }) as MiAnnouncement & { isRead?: boolean | null };
+
+ if (me && announcement.isRead === undefined) {
+ announcement.isRead = await this.announcementReadsRepository
+ .countBy({
+ announcementId: announcement.id,
+ userId: me.id,
+ })
+ .then((count: number) => count > 0);
+ }
+
+ return {
+ id: announcement.id,
+ createdAt: this.idService.parse(announcement.id).date.toISOString(),
+ updatedAt: announcement.updatedAt?.toISOString() ?? null,
+ title: announcement.title,
+ text: announcement.text,
+ imageUrl: announcement.imageUrl,
+ icon: announcement.icon,
+ display: announcement.display,
+ forYou: announcement.userId === me?.id,
+ needConfirmationToRead: announcement.needConfirmationToRead,
+ silence: announcement.silence,
+ isRead: announcement.isRead !== null ? announcement.isRead : undefined,
+ };
+ }
+
+ @bindThis
+ public async packMany(
+ announcements: (MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null } | MiAnnouncement)[],
+ me?: { id: MiUser['id'] } | null | undefined,
+ ) : Promise<Packed<'Announcement'>[]> {
+ return (await Promise.allSettled(announcements.map(x => this.pack(x, me))))
+ .filter(result => result.status === 'fulfilled')
+ .map(result => (result as PromiseFulfilledResult<Packed<'Announcement'>>).value);
+ }
+}
diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts
index 64d6a3c978..e770028af3 100644
--- a/packages/backend/src/core/entities/AntennaEntityService.ts
+++ b/packages/backend/src/core/entities/AntennaEntityService.ts
@@ -38,11 +38,12 @@ export class AntennaEntityService {
users: antenna.users,
caseSensitive: antenna.caseSensitive,
localOnly: antenna.localOnly,
- notify: antenna.notify,
+ excludeBots: antenna.excludeBots,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote: false, // TODO
+ notify: false, // 後方互換性のため
};
}
}
diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts
index c8c1520ceb..1e699032e2 100644
--- a/packages/backend/src/core/entities/BlockingEntityService.ts
+++ b/packages/backend/src/core/entities/BlockingEntityService.ts
@@ -29,6 +29,9 @@ export class BlockingEntityService {
public async pack(
src: MiBlocking['id'] | MiBlocking,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ blockee?: Packed<'UserDetailedNotMe'>,
+ },
): Promise<Packed<'Blocking'>> {
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
@@ -36,17 +39,20 @@ export class BlockingEntityService {
id: blocking.id,
createdAt: this.idService.parse(blocking.id).date.toISOString(),
blockeeId: blocking.blockeeId,
- blockee: this.userEntityService.pack(blocking.blockeeId, me, {
+ blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- blockings: any[],
+ public async packMany(
+ blockings: MiBlocking[],
me: { id: MiUser['id'] },
) {
- return Promise.all(blockings.map(x => this.pack(x, me)));
+ const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId);
+ const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) })));
}
}
diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts
index 26fcd6714d..3855a28436 100644
--- a/packages/backend/src/core/entities/ClipEntityService.ts
+++ b/packages/backend/src/core/entities/ClipEntityService.ts
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import type { ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js';
+import type { ClipNotesRepository, ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
@@ -20,6 +20,9 @@ export class ClipEntityService {
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
+ @Inject(DI.clipNotesRepository)
+ private clipNotesRepository: ClipNotesRepository,
+
@Inject(DI.clipFavoritesRepository)
private clipFavoritesRepository: ClipFavoritesRepository,
@@ -32,6 +35,9 @@ export class ClipEntityService {
public async pack(
src: MiClip['id'] | MiClip,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise<Packed<'Clip'>> {
const meId = me ? me.id : null;
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
@@ -41,21 +47,25 @@ export class ClipEntityService {
createdAt: this.idService.parse(clip.id).date.toISOString(),
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
userId: clip.userId,
- user: this.userEntityService.pack(clip.user ?? clip.userId),
+ user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId),
name: clip.name,
description: clip.description,
isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
+ notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
});
}
@bindThis
- public packMany(
+ public async packMany(
clips: MiClip[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(clips.map(x => this.pack(x, me)));
+ const _users = clips.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(clips.map(clip => this.pack(clip, me, { packedUser: _userMap.get(clip.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index 8affe2b3bf..02ff2e7754 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -222,6 +222,9 @@ export class DriveFileEntityService {
public async packNullable(
src: MiDriveFile['id'] | MiDriveFile,
options?: PackOptions,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({
detail: false,
@@ -248,8 +251,8 @@ export class DriveFileEntityService {
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,
+ userId: file.userId,
+ user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
});
}
@@ -258,7 +261,10 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
- const items = await Promise.all(files.map(f => this.packNullable(f, options)));
+ const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
+ const _userMap = await this.userEntityService.packMany(_user)
+ .then(users => new Map(users.map(user => [user.id, user])));
+ const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
return items.filter(isNotNull);
}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index db4cf6d360..d110f7afc6 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -33,6 +33,9 @@ export class FlashEntityService {
public async pack(
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise<Packed<'Flash'>> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
@@ -42,7 +45,7 @@ export class FlashEntityService {
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
- user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
title: flash.title,
summary: flash.summary,
script: flash.script,
@@ -52,11 +55,14 @@ export class FlashEntityService {
}
@bindThis
- public packMany(
- flashs: MiFlash[],
+ public async packMany(
+ flashes: MiFlash[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(flashs.map(x => this.pack(x, me)));
+ const _users = flashes.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts
index 763b75101f..0101ec8aa7 100644
--- a/packages/backend/src/core/entities/FollowRequestEntityService.ts
+++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts
@@ -10,6 +10,7 @@ import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFollowRequest } from '@/models/FollowRequest.js';
import { bindThis } from '@/decorators.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -26,14 +27,36 @@ export class FollowRequestEntityService {
public async pack(
src: MiFollowRequest['id'] | MiFollowRequest,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedFollower?: Packed<'UserLite'>,
+ packedFollowee?: Packed<'UserLite'>,
+ },
) {
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),
+ follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me),
+ followee: hint?.packedFollowee ?? await this.userEntityService.pack(request.followeeId, me),
};
}
+
+ @bindThis
+ public async packMany(
+ requests: MiFollowRequest[],
+ me?: { id: MiUser['id'] } | null | undefined,
+ ) {
+ const _followers = requests.map(({ follower, followerId }) => follower ?? followerId);
+ const _followees = requests.map(({ followee, followeeId }) => followee ?? followeeId);
+ const _userMap = await this.userEntityService.packMany([..._followers, ..._followees], me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ requests.map(req => {
+ const packedFollower = _userMap.get(req.followerId);
+ const packedFollowee = _userMap.get(req.followeeId);
+ return this.pack(req, me, { packedFollower, packedFollowee });
+ }),
+ );
+ }
}
diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts
index 24cd33e3f7..d2dbaf2270 100644
--- a/packages/backend/src/core/entities/FollowingEntityService.ts
+++ b/packages/backend/src/core/entities/FollowingEntityService.ts
@@ -78,6 +78,10 @@ export class FollowingEntityService {
populateFollowee?: boolean;
populateFollower?: boolean;
},
+ hint?: {
+ packedFollowee?: Packed<'UserDetailedNotMe'>,
+ packedFollower?: Packed<'UserDetailedNotMe'>,
+ },
): Promise<Packed<'Following'>> {
const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src });
@@ -88,25 +92,35 @@ export class FollowingEntityService {
createdAt: this.idService.parse(following.id).date.toISOString(),
followeeId: following.followeeId,
followerId: following.followerId,
- followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
+ followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
- follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
+ follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
});
}
@bindThis
- public packMany(
- followings: any[],
+ public async packMany(
+ followings: MiFollowing[],
me?: { id: MiUser['id'] } | null | undefined,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
},
) {
- return Promise.all(followings.map(x => this.pack(x, me, opts)));
+ const _followees = opts?.populateFollowee ? followings.map(({ followee, followeeId }) => followee ?? followeeId) : [];
+ const _followers = opts?.populateFollower ? followings.map(({ follower, followerId }) => follower ?? followerId) : [];
+ const _userMap = await this.userEntityService.packMany([..._followees, ..._followers], me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ followings.map(following => {
+ const packedFollowee = opts?.populateFollowee ? _userMap.get(following.followeeId) : undefined;
+ const packedFollower = opts?.populateFollower ? _userMap.get(following.followerId) : undefined;
+ return this.pack(following, me, opts, { packedFollowee, packedFollower });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts
index 101182a9e5..9746a4c1af 100644
--- a/packages/backend/src/core/entities/GalleryPostEntityService.ts
+++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts
@@ -35,6 +35,9 @@ export class GalleryPostEntityService {
public async pack(
src: MiGalleryPost['id'] | MiGalleryPost,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise<Packed<'GalleryPost'>> {
const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src });
@@ -44,7 +47,7 @@ export class GalleryPostEntityService {
createdAt: this.idService.parse(post.id).date.toISOString(),
updatedAt: post.updatedAt.toISOString(),
userId: post.userId,
- user: this.userEntityService.pack(post.user ?? post.userId, me),
+ user: hint?.packedUser ?? this.userEntityService.pack(post.user ?? post.userId, me),
title: post.title,
description: post.description,
fileIds: post.fileIds,
@@ -58,11 +61,14 @@ export class GalleryPostEntityService {
}
@bindThis
- public packMany(
+ public async packMany(
posts: MiGalleryPost[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(posts.map(x => this.pack(x, me)));
+ const _users = posts.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(posts.map(post => this.pack(post, me, { packedUser: _userMap.get(post.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index e46bd8b963..9117b13914 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -39,7 +39,8 @@ export class InstanceEntityService {
followingCount: instance.followingCount,
followersCount: instance.followersCount,
isNotResponding: instance.isNotResponding,
- isSuspended: instance.isSuspended,
+ isSuspended: instance.suspensionState !== 'none',
+ suspensionState: instance.suspensionState,
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts
index 891543bc0f..26f57e1299 100644
--- a/packages/backend/src/core/entities/InviteCodeEntityService.ts
+++ b/packages/backend/src/core/entities/InviteCodeEntityService.ts
@@ -12,6 +12,7 @@ import type { MiUser } from '@/models/User.js';
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -29,6 +30,10 @@ export class InviteCodeEntityService {
public async pack(
src: MiRegistrationTicket['id'] | MiRegistrationTicket,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedCreatedBy?: Packed<'UserLite'>,
+ packedUsedBy?: Packed<'UserLite'>,
+ },
): Promise<Packed<'InviteCode'>> {
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
where: {
@@ -42,18 +47,28 @@ export class InviteCodeEntityService {
code: target.code,
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
createdAt: this.idService.parse(target.id).date.toISOString(),
- createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null,
- usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null,
+ createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null,
+ usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null,
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
used: !!target.usedAt,
});
}
@bindThis
- public packMany(
- targets: any[],
+ public async packMany(
+ tickets: MiRegistrationTicket[],
me: { id: MiUser['id'] },
) {
- return Promise.all(targets.map(x => this.pack(x, me)));
+ const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull);
+ const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull);
+ const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ tickets.map(ticket => {
+ const packedCreatedBy = ticket.createdById != null ? _userMap.get(ticket.createdById) : undefined;
+ const packedUsedBy = ticket.usedById != null ? _userMap.get(ticket.usedById) : undefined;
+ return this.pack(ticket, me, { packedCreatedBy, packedUsedBy });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index b50d76288f..5dfec589e1 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -67,6 +67,7 @@ export class MetaEntityService {
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
+ inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
@@ -111,6 +112,7 @@ export class MetaEntityService {
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
+ enableUrlPreview: instance.urlPreviewEnabled,
};
return packed;
diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts
index 205e147bd1..bf1b2a002c 100644
--- a/packages/backend/src/core/entities/ModerationLogEntityService.ts
+++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts
@@ -8,9 +8,10 @@ import { DI } from '@/di-symbols.js';
import type { ModerationLogsRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { } from '@/models/Blocking.js';
-import type { MiModerationLog } from '@/models/ModerationLog.js';
+import { MiModerationLog } from '@/models/ModerationLog.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -27,6 +28,9 @@ export class ModerationLogEntityService {
@bindThis
public async pack(
src: MiModerationLog['id'] | MiModerationLog,
+ hint?: {
+ packedUser?: Packed<'UserDetailedNotMe'>,
+ },
) {
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
@@ -36,17 +40,20 @@ export class ModerationLogEntityService {
type: log.type,
info: log.info,
userId: log.userId,
- user: this.userEntityService.pack(log.user ?? log.userId, null, {
+ user: hint?.packedUser ?? this.userEntityService.pack(log.user ?? log.userId, null, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- reports: any[],
+ public async packMany(
+ reports: MiModerationLog[],
) {
- return Promise.all(reports.map(x => this.pack(x)));
+ const _users = reports.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, null, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(reports.map(report => this.pack(report, { packedUser: _userMap.get(report.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts
index 0a52f429a2..d361a20271 100644
--- a/packages/backend/src/core/entities/MutingEntityService.ts
+++ b/packages/backend/src/core/entities/MutingEntityService.ts
@@ -30,6 +30,9 @@ export class MutingEntityService {
public async pack(
src: MiMuting['id'] | MiMuting,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedMutee?: Packed<'UserDetailedNotMe'>,
+ },
): Promise<Packed<'Muting'>> {
const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src });
@@ -38,18 +41,21 @@ export class MutingEntityService {
createdAt: this.idService.parse(muting.id).date.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId,
- mutee: this.userEntityService.pack(muting.muteeId, me, {
+ mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- mutings: any[],
+ public async packMany(
+ mutings: MiMuting[],
me: { id: MiUser['id'] },
) {
- return Promise.all(mutings.map(x => this.pack(x, me)));
+ const _mutees = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
+ const _userMap = await this.userEntityService.packMany(_mutees, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
}
}
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 5b6affc6a5..2ce72c50b8 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -290,6 +290,7 @@ export class NoteEntityService implements OnModuleInit {
_hint_?: {
myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
+ packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
};
},
): Promise<Packed<'Note'>> {
@@ -319,12 +320,13 @@ export class NoteEntityService implements OnModuleInit {
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packedFiles = options?._hint_?.packedFiles;
+ const packedUsers = options?._hint_?.packedUsers;
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(),
userId: note.userId,
- user: this.userEntityService.pack(note.user ?? note.userId, me),
+ user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
text: text,
cw: note.cw,
visibility: note.visibility,
@@ -333,6 +335,7 @@ export class NoteEntityService implements OnModuleInit {
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
+ reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
@@ -448,12 +451,20 @@ export class NoteEntityService implements OnModuleInit {
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
+ const users = [
+ ...notes.map(({ user, userId }) => user ?? userId),
+ ...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
+ ...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
+ ];
+ const packedUsers = await this.userEntityService.packMany(users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
_hint_: {
myReactions: myReactionsMap,
packedFiles,
+ packedUsers,
},
})));
}
diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts
index 3f4fa3cf96..46ec13704c 100644
--- a/packages/backend/src/core/entities/NoteReactionEntityService.ts
+++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts
@@ -52,6 +52,9 @@ export class NoteReactionEntityService implements OnModuleInit {
options?: {
withNote: boolean;
},
+ hints?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({
withNote: false,
@@ -62,7 +65,7 @@ export class NoteReactionEntityService implements OnModuleInit {
return {
id: reaction.id,
createdAt: this.idService.parse(reaction.id).date.toISOString(),
- user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
+ user: hints?.packedUser ?? 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),
@@ -81,7 +84,9 @@ export class NoteReactionEntityService implements OnModuleInit {
const opts = Object.assign({
withNote: false,
}, options);
-
- return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts)));
+ const _users = reactions.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts
index 65c69a49a7..142d9e81db 100644
--- a/packages/backend/src/core/entities/PageEntityService.ts
+++ b/packages/backend/src/core/entities/PageEntityService.ts
@@ -40,6 +40,9 @@ export class PageEntityService {
public async pack(
src: MiPage['id'] | MiPage,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise<Packed<'Page'>> {
const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
@@ -91,7 +94,7 @@ export class PageEntityService {
createdAt: this.idService.parse(page.id).date.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
- user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: hint?.packedUser ?? this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
content: page.content,
variables: page.variables,
title: page.title,
@@ -110,11 +113,14 @@ export class PageEntityService {
}
@bindThis
- public packMany(
+ public async packMany(
pages: MiPage[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(pages.map(x => this.pack(x, me)));
+ const _users = pages.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(pages.map(page => this.pack(page, me, { packedUser: _userMap.get(page.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
index 0b05a5db80..e4e154109a 100644
--- a/packages/backend/src/core/entities/RenoteMutingEntityService.ts
+++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
@@ -30,6 +30,9 @@ export class RenoteMutingEntityService {
public async pack(
src: MiRenoteMuting['id'] | MiRenoteMuting,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedMutee?: Packed<'UserDetailedNotMe'>
+ },
): Promise<Packed<'RenoteMuting'>> {
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
@@ -37,18 +40,21 @@ export class RenoteMutingEntityService {
id: muting.id,
createdAt: this.idService.parse(muting.id).date.toISOString(),
muteeId: muting.muteeId,
- mutee: this.userEntityService.pack(muting.muteeId, me, {
+ mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- mutings: any[],
+ public async packMany(
+ mutings: MiRenoteMuting[],
me: { id: MiUser['id'] },
) {
- return Promise.all(mutings.map(x => this.pack(x, me)));
+ const _users = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
}
}
diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts
index 32cbe631e4..df042e75c1 100644
--- a/packages/backend/src/core/entities/ReversiGameEntityService.ts
+++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts
@@ -28,13 +28,15 @@ export class ReversiGameEntityService {
@bindThis
public async packDetail(
src: MiReversiGame['id'] | MiReversiGame,
+ hint?: {
+ packedUser1?: Packed<'UserLite'>,
+ packedUser2?: Packed<'UserLite'>,
+ },
): Promise<Packed<'ReversiGameDetailed'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
- const users = await Promise.all([
- this.userEntityService.pack(game.user1 ?? game.user1Id),
- this.userEntityService.pack(game.user2 ?? game.user2Id),
- ]);
+ const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
+ const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
return await awaitAll({
id: game.id,
@@ -49,10 +51,10 @@ export class ReversiGameEntityService {
user2Ready: game.user2Ready,
user1Id: game.user1Id,
user2Id: game.user2Id,
- user1: users[0],
- user2: users[1],
+ user1,
+ user2,
winnerId: game.winnerId,
- winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
+ winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
@@ -68,22 +70,35 @@ export class ReversiGameEntityService {
}
@bindThis
- public packDetailMany(
- xs: MiReversiGame[],
+ public async packDetailMany(
+ games: MiReversiGame[],
) {
- return Promise.all(xs.map(x => this.packDetail(x)));
+ const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
+ const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
+ const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ games.map(game => {
+ return this.packDetail(game, {
+ packedUser1: _userMap.get(game.user1Id),
+ packedUser2: _userMap.get(game.user2Id),
+ });
+ }),
+ );
}
@bindThis
public async packLite(
src: MiReversiGame['id'] | MiReversiGame,
+ hint?: {
+ packedUser1?: Packed<'UserLite'>,
+ packedUser2?: Packed<'UserLite'>,
+ },
): Promise<Packed<'ReversiGameLite'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
- const users = await Promise.all([
- this.userEntityService.pack(game.user1 ?? game.user1Id),
- this.userEntityService.pack(game.user2 ?? game.user2Id),
- ]);
+ const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
+ const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
return await awaitAll({
id: game.id,
@@ -94,10 +109,10 @@ export class ReversiGameEntityService {
isEnded: game.isEnded,
user1Id: game.user1Id,
user2Id: game.user2Id,
- user1: users[0],
- user2: users[1],
+ user1,
+ user2,
winnerId: game.winnerId,
- winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
+ winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
@@ -111,10 +126,21 @@ export class ReversiGameEntityService {
}
@bindThis
- public packLiteMany(
- xs: MiReversiGame[],
+ public async packLiteMany(
+ games: MiReversiGame[],
) {
- return Promise.all(xs.map(x => this.packLite(x)));
+ const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
+ const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
+ const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ games.map(game => {
+ return this.packLite(game, {
+ packedUser1: _userMap.get(game.user1Id),
+ packedUser2: _userMap.get(game.user2Id),
+ });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 14761357a5..b80a1ec206 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import _Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
+import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -14,9 +15,30 @@ 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 type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
-import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
-import { MiNotification } from '@/models/Notification.js';
-import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
+import {
+ birthdaySchema,
+ descriptionSchema,
+ localUsernameSchema,
+ locationSchema,
+ nameSchema,
+ passwordSchema,
+} from '@/models/User.js';
+import type {
+ BlockingsRepository,
+ FollowingsRepository,
+ FollowRequestsRepository,
+ MiFollowing,
+ MiUserNotePining,
+ MiUserProfile,
+ MutingsRepository,
+ NoteUnreadsRepository,
+ RenoteMutingsRepository,
+ UserMemoRepository,
+ UserNotePiningsRepository,
+ UserProfilesRepository,
+ UserSecurityKeysRepository,
+ UsersRepository,
+} from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@@ -46,11 +68,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
return !isLocalUser(user);
}
+export type UserRelation = {
+ id: MiUser['id']
+ following: MiFollowing | null,
+ isFollowing: boolean
+ isFollowed: boolean
+ hasPendingFollowRequestFromYou: boolean
+ hasPendingFollowRequestToYou: boolean
+ isBlocking: boolean
+ isBlocked: boolean
+ isMuted: boolean
+ isRenoteMuted: boolean
+}
+
@Injectable()
export class UserEntityService implements OnModuleInit {
private apPersonService: ApPersonService;
private noteEntityService: NoteEntityService;
- private driveFileEntityService: DriveFileEntityService;
private pageEntityService: PageEntityService;
private customEmojiService: CustomEmojiService;
private announcementService: AnnouncementService;
@@ -89,9 +123,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
- @Inject(DI.driveFilesRepository)
- private driveFilesRepository: DriveFilesRepository,
-
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@@ -101,12 +132,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
- @Inject(DI.announcementReadsRepository)
- private announcementReadsRepository: AnnouncementReadsRepository,
-
- @Inject(DI.announcementsRepository)
- private announcementsRepository: AnnouncementsRepository,
-
@Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository,
) {
@@ -115,7 +140,6 @@ export class UserEntityService implements OnModuleInit {
onModuleInit() {
this.apPersonService = this.moduleRef.get('ApPersonService');
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.announcementService = this.moduleRef.get('AnnouncementService');
@@ -138,7 +162,7 @@ export class UserEntityService implements OnModuleInit {
public isRemoteUser = isRemoteUser;
@bindThis
- public async getRelation(me: MiUser['id'], target: MiUser['id']) {
+ public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise<UserRelation> {
const [
following,
isFollowed,
@@ -212,6 +236,80 @@ export class UserEntityService implements OnModuleInit {
}
@bindThis
+ public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise<Map<MiUser['id'], UserRelation>> {
+ const [
+ followers,
+ followees,
+ followersRequests,
+ followeesRequests,
+ blockers,
+ blockees,
+ muters,
+ renoteMuters,
+ ] = await Promise.all([
+ this.followingsRepository.findBy({ followerId: me })
+ .then(f => new Map(f.map(it => [it.followeeId, it]))),
+ this.followingsRepository.createQueryBuilder('f')
+ .select('f.followerId')
+ .where('f.followeeId = :me', { me })
+ .getRawMany<{ f_followerId: string }>()
+ .then(it => it.map(it => it.f_followerId)),
+ this.followRequestsRepository.createQueryBuilder('f')
+ .select('f.followeeId')
+ .where('f.followerId = :me', { me })
+ .getRawMany<{ f_followeeId: string }>()
+ .then(it => it.map(it => it.f_followeeId)),
+ this.followRequestsRepository.createQueryBuilder('f')
+ .select('f.followerId')
+ .where('f.followeeId = :me', { me })
+ .getRawMany<{ f_followerId: string }>()
+ .then(it => it.map(it => it.f_followerId)),
+ this.blockingsRepository.createQueryBuilder('b')
+ .select('b.blockeeId')
+ .where('b.blockerId = :me', { me })
+ .getRawMany<{ b_blockeeId: string }>()
+ .then(it => it.map(it => it.b_blockeeId)),
+ this.blockingsRepository.createQueryBuilder('b')
+ .select('b.blockerId')
+ .where('b.blockeeId = :me', { me })
+ .getRawMany<{ b_blockerId: string }>()
+ .then(it => it.map(it => it.b_blockerId)),
+ this.mutingsRepository.createQueryBuilder('m')
+ .select('m.muteeId')
+ .where('m.muterId = :me', { me })
+ .getRawMany<{ m_muteeId: string }>()
+ .then(it => it.map(it => it.m_muteeId)),
+ this.renoteMutingsRepository.createQueryBuilder('m')
+ .select('m.muteeId')
+ .where('m.muterId = :me', { me })
+ .getRawMany<{ m_muteeId: string }>()
+ .then(it => it.map(it => it.m_muteeId)),
+ ]);
+
+ return new Map(
+ targets.map(target => {
+ const following = followers.get(target) ?? null;
+
+ return [
+ target,
+ {
+ id: target,
+ following: following,
+ isFollowing: following != null,
+ isFollowed: followees.includes(target),
+ hasPendingFollowRequestFromYou: followersRequests.includes(target),
+ hasPendingFollowRequestToYou: followeesRequests.includes(target),
+ isBlocking: blockers.includes(target),
+ isBlocked: blockees.includes(target),
+ isMuted: muters.includes(target),
+ isRenoteMuted: renoteMuters.includes(target),
+ },
+ ];
+ }),
+ );
+ }
+
+ @bindThis
public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> {
/*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
@@ -303,6 +401,9 @@ export class UserEntityService implements OnModuleInit {
schema?: S,
includeSecrets?: boolean,
userProfile?: MiUserProfile,
+ userRelations?: Map<MiUser['id'], UserRelation>,
+ userMemos?: Map<MiUser['id'], string | null>,
+ pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
},
): Promise<Packed<S>> {
const opts = Object.assign({
@@ -317,13 +418,41 @@ export class UserEntityService implements OnModuleInit {
const isMe = meId === user.id;
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
- const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
- const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
- .where('pin.userId = :userId', { userId: user.id })
- .innerJoinAndSelect('pin.note', 'note')
- .orderBy('pin.id', 'DESC')
- .getMany() : [];
- const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
+ const profile = isDetailed
+ ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
+ : null;
+
+ let relation: UserRelation | null = null;
+ if (meId && !isMe && isDetailed) {
+ if (opts.userRelations) {
+ relation = opts.userRelations.get(user.id) ?? null;
+ } else {
+ relation = await this.getRelation(meId, user.id);
+ }
+ }
+
+ let memo: string | null = null;
+ if (isDetailed && meId) {
+ if (opts.userMemos) {
+ memo = opts.userMemos.get(user.id) ?? null;
+ } else {
+ memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id })
+ .then(row => row?.memo ?? null);
+ }
+ }
+
+ let pins: MiUserNotePining[] = [];
+ if (isDetailed) {
+ if (opts.pinNotes) {
+ pins = opts.pinNotes.get(user.id) ?? [];
+ } else {
+ pins = await this.userNotePiningsRepository.createQueryBuilder('pin')
+ .where('pin.userId = :userId', { userId: user.id })
+ .innerJoinAndSelect('pin.note', 'note')
+ .orderBy('pin.id', 'DESC')
+ .getMany();
+ }
+ }
const followingCount = profile == null ? null :
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
@@ -416,9 +545,7 @@ export class UserEntityService implements OnModuleInit {
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
- ? this.userSecurityKeysRepository.countBy({
- userId: user.id,
- }).then(result => result >= 1)
+ ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
: false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
@@ -430,10 +557,7 @@ export class UserEntityService implements OnModuleInit {
isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder,
}))),
- memo: meId == null ? null : await this.userMemosRepository.findOneBy({
- userId: meId,
- targetUserId: user.id,
- }).then(row => row?.memo ?? null),
+ memo: memo,
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
@@ -514,7 +638,7 @@ export class UserEntityService implements OnModuleInit {
return await awaitAll(packed);
}
- public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
+ public async packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
users: (MiUser['id'] | MiUser)[],
me?: { id: MiUser['id'] } | null | undefined,
options?: {
@@ -522,6 +646,69 @@ export class UserEntityService implements OnModuleInit {
includeSecrets?: boolean,
},
): Promise<Packed<S>[]> {
- return Promise.all(users.map(u => this.pack(u, me, options)));
+ // -- IDのみの要素を補完して完全なエンティティ一覧を作る
+
+ const _users = users.filter((user): user is MiUser => typeof user !== 'string');
+ if (_users.length !== users.length) {
+ _users.push(
+ ...await this.usersRepository.findBy({
+ id: In(users.filter((user): user is string => typeof user === 'string')),
+ }),
+ );
+ }
+ const _userIds = _users.map(u => u.id);
+
+ // -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
+
+ let profilesMap: Map<MiUser['id'], MiUserProfile> = new Map();
+ let userRelations: Map<MiUser['id'], UserRelation> = new Map();
+ let userMemos: Map<MiUser['id'], string | null> = new Map();
+ let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map();
+
+ if (options?.schema !== 'UserLite') {
+ profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
+ .then(profiles => new Map(profiles.map(p => [p.userId, p])));
+
+ const meId = me ? me.id : null;
+ if (meId) {
+ userMemos = await this.userMemosRepository.findBy({ userId: meId })
+ .then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo])));
+
+ if (_userIds.length > 0) {
+ userRelations = await this.getRelations(meId, _userIds);
+ pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin')
+ .where('pin.userId IN (:...userIds)', { userIds: _userIds })
+ .innerJoinAndSelect('pin.note', 'note')
+ .getMany()
+ .then(pinsNotes => {
+ const map = new Map<MiUser['id'], MiUserNotePining[]>();
+ for (const note of pinsNotes) {
+ const notes = map.get(note.userId) ?? [];
+ notes.push(note);
+ map.set(note.userId, notes);
+ }
+ for (const [, notes] of map.entries()) {
+ // pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく
+ notes.sort((a, b) => b.id.localeCompare(a.id));
+ }
+ return map;
+ });
+ }
+ }
+ }
+
+ return Promise.all(
+ _users.map(u => this.pack(
+ u,
+ me,
+ {
+ ...options,
+ userProfile: profilesMap.get(u.id),
+ userRelations: userRelations,
+ userMemos: userMemos,
+ pinNotes: pinNotes,
+ },
+ )),
+ );
}
}
diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts
index 09cab24521..b77249c5cb 100644
--- a/packages/backend/src/core/entities/UserListEntityService.ts
+++ b/packages/backend/src/core/entities/UserListEntityService.ts
@@ -50,11 +50,14 @@ export class UserListEntityService {
public async packMembershipsMany(
memberships: MiUserListMembership[],
) {
+ const _users = memberships.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users)
+ .then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(memberships.map(async x => ({
id: x.id,
createdAt: this.idService.parse(x.id).date.toISOString(),
userId: x.userId,
- user: await this.userEntityService.pack(x.userId),
+ user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId),
withReplies: x.withReplies,
})));
}
diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts
index 49a48f6a6b..3e1c099e00 100644
--- a/packages/backend/src/misc/fastify-hook-handlers.ts
+++ b/packages/backend/src/misc/fastify-hook-handlers.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import type { onRequestHookHandler } from 'fastify';
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts
index 62a8ab8ace..342e0f8602 100644
--- a/packages/backend/src/misc/gen-identicon.ts
+++ b/packages/backend/src/misc/gen-identicon.ts
@@ -8,9 +8,8 @@
* https://en.wikipedia.org/wiki/Identicon
*/
-import * as p from 'pureimage';
+import { createCanvas } from '@napi-rs/canvas';
import gen from 'random-seed';
-import type { WriteStream } from 'node:fs';
const size = 128; // px
const n = 5; // resolution
@@ -45,9 +44,9 @@ const sideN = Math.floor(n / 2);
/**
* Generate buffer of an identicon by seed
*/
-export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
+export async function genIdenticon(seed: string): Promise<Buffer> {
const rand = gen.create(seed);
- const canvas = p.make(size, size, undefined);
+ const canvas = createCanvas(size, size);
const ctx = canvas.getContext('2d');
const bgColors = colors[rand(colors.length)];
@@ -101,5 +100,5 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
}
}
- return p.encodePNGToStream(canvas, stream);
+ return await canvas.encode('png');
}
diff --git a/packages/backend/src/misc/is-pure-renote.ts b/packages/backend/src/misc/is-pure-renote.ts
deleted file mode 100644
index 994d981522..0000000000
--- a/packages/backend/src/misc/is-pure-renote.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { MiNote } from '@/models/Note.js';
-
-export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
- if (!note.renoteId) return false;
-
- if (note.text) return false; // it's quoted with text
- if (note.fileIds.length !== 0) return false; // it's quoted with files
- if (note.hasPoll) return false; // it's quoted with poll
- return true;
-}
diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts
deleted file mode 100644
index 75b29f63f4..0000000000
--- a/packages/backend/src/misc/is-quote.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { MiNote } from '@/models/Note.js';
-
-// eslint-disable-next-line import/no-default-export
-export default function(note: MiNote): boolean {
- // sync with NoteCreateService.isQuote
- return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
-}
diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts
new file mode 100644
index 0000000000..48f821806c
--- /dev/null
+++ b/packages/backend/src/misc/is-renote.ts
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { MiNote } from '@/models/Note.js';
+import type { Packed } from '@/misc/json-schema.js';
+
+type Renote =
+ MiNote & {
+ renoteId: NonNullable<MiNote['renoteId']>
+ };
+
+type Quote =
+ Renote & ({
+ text: NonNullable<MiNote['text']>
+ } | {
+ cw: NonNullable<MiNote['cw']>
+ } | {
+ replyId: NonNullable<MiNote['replyId']>
+ reply: NonNullable<MiNote['reply']>
+ } | {
+ hasPoll: true
+ });
+
+export function isRenote(note: MiNote): note is Renote {
+ return note.renoteId != null;
+}
+
+export function isQuote(note: Renote): note is Quote {
+ // NOTE: SYNC WITH NoteCreateService.isQuote
+ return note.text != null ||
+ note.cw != null ||
+ note.replyId != null ||
+ note.hasPoll ||
+ note.fileIds.length > 0;
+}
+
+type PackedRenote =
+ Packed<'Note'> & {
+ renoteId: NonNullable<Packed<'Note'>['renoteId']>
+ };
+
+type PackedQuote =
+ PackedRenote & ({
+ text: NonNullable<Packed<'Note'>['text']>
+ } | {
+ cw: NonNullable<Packed<'Note'>['cw']>
+ } | {
+ replyId: NonNullable<Packed<'Note'>['replyId']>
+ } | {
+ poll: NonNullable<Packed<'Note'>['poll']>
+ } | {
+ fileIds: NonNullable<Packed<'Note'>['fileIds']>
+ });
+
+export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote {
+ return note.renoteId != null;
+}
+
+export function isQuotePacked(note: PackedRenote): note is PackedQuote {
+ return note.text != null ||
+ note.cw != null ||
+ note.replyId != null ||
+ note.poll != null ||
+ (note.fileIds != null && note.fileIds.length > 0);
+}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 46b0bb2fab..41e5bfe9e4 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -48,6 +48,7 @@ import {
packedRoleCondFormulaValueCreatedSchema,
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaValueSchema,
+ packedRoleCondFormulaValueUserSettingBooleanSchema,
} from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
@@ -97,6 +98,7 @@ export const refs = {
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
+ RoleCondFormulaValueUserSettingBooleanSchema: packedRoleCondFormulaValueUserSettingBooleanSchema,
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
@@ -226,7 +228,7 @@ export type SchemaTypeDef<p extends Schema> =
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
never
) :
- p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] :
+ p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
any[]
) :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
diff --git a/packages/backend/src/misc/loader.ts b/packages/backend/src/misc/loader.ts
index 25f7b54d31..7f29b9db10 100644
--- a/packages/backend/src/misc/loader.ts
+++ b/packages/backend/src/misc/loader.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export type FetchFunction<K, V> = (key: K) => Promise<V>;
type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>;
diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts
index 332a899768..33e6f48189 100644
--- a/packages/backend/src/models/Antenna.ts
+++ b/packages/backend/src/models/Antenna.ts
@@ -75,6 +75,11 @@ export class MiAntenna {
@Column('boolean', {
default: false,
})
+ public excludeBots: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
public withReplies: boolean;
@Column('boolean')
@@ -85,9 +90,6 @@ export class MiAntenna {
})
public expression: string | null;
- @Column('boolean')
- public notify: boolean;
-
@Index()
@Column('boolean', {
default: true,
diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts
index 9863c9d75d..17cd5c6665 100644
--- a/packages/backend/src/models/Instance.ts
+++ b/packages/backend/src/models/Instance.ts
@@ -81,13 +81,22 @@ export class MiInstance {
public isNotResponding: boolean;
/**
- * このインスタンスへの配信を停止するか
+ * このインスタンスと不通になった日時
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public notRespondingSince: Date | null;
+
+ /**
+ * このインスタンスへの配信状態
*/
@Index()
- @Column('boolean', {
- default: false,
+ @Column('enum', {
+ default: 'none',
+ enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
})
- public isSuspended: boolean;
+ public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
@Column('varchar', {
length: 64, nullable: true,
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 66f19ce197..ad306fcad6 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -277,12 +277,6 @@ export class MiMeta {
})
public enableSensitiveMediaDetectionForVideos: boolean;
- @Column('varchar', {
- length: 1024,
- nullable: true,
- })
- public summalyProxy: string | null;
-
@Column('boolean', {
default: false,
})
@@ -383,6 +377,12 @@ export class MiMeta {
public privacyPolicyUrl: string | null;
@Column('varchar', {
+ length: 1024,
+ nullable: true,
+ })
+ public inquiryUrl: string | null;
+
+ @Column('varchar', {
length: 8192,
nullable: true,
})
@@ -588,4 +588,36 @@ export class MiMeta {
default: 0,
})
public notesPerOneAd: number;
+
+ @Column('boolean', {
+ default: true,
+ })
+ public urlPreviewEnabled: boolean;
+
+ @Column('integer', {
+ default: 10000,
+ })
+ public urlPreviewTimeout: number;
+
+ @Column('bigint', {
+ default: 1024 * 1024 * 10,
+ })
+ public urlPreviewMaximumContentLength: number;
+
+ @Column('boolean', {
+ default: true,
+ })
+ public urlPreviewRequireContentLength: boolean;
+
+ @Column('varchar', {
+ length: 1024,
+ nullable: true,
+ })
+ public urlPreviewSummaryProxyUrl: string | null;
+
+ @Column('varchar', {
+ length: 1024,
+ nullable: true,
+ })
+ public urlPreviewUserAgent: string | null;
}
diff --git a/packages/backend/src/models/Poll.ts b/packages/backend/src/models/Poll.ts
index c2693dbb19..ca985c8b24 100644
--- a/packages/backend/src/models/Poll.ts
+++ b/packages/backend/src/models/Poll.ts
@@ -8,6 +8,7 @@ import { noteVisibilities } from '@/types.js';
import { id } from './util/id.js';
import { MiNote } from './Note.js';
import type { MiUser } from './User.js';
+import type { MiChannel } from "@/models/Channel.js";
@Entity('poll')
export class MiPoll {
@@ -58,6 +59,14 @@ export class MiPoll {
comment: '[Denormalized]',
})
public userHost: string | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: '[Denormalized]',
+ })
+ public channelId: MiChannel['id'] | null;
//#endregion
constructor(data: Partial<MiPoll>) {
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index bd447570dd..d3062d6b36 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -5,409 +5,409 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js';
+import { MiRepository, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, miRepository } from './_.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
const $usersRepository: Provider = {
provide: DI.usersRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUser),
+ useFactory: (db: DataSource) => db.getRepository(MiUser).extend(miRepository as MiRepository<MiUser>),
inject: [DI.db],
};
const $notesRepository: Provider = {
provide: DI.notesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNote),
+ useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository<MiNote>),
inject: [DI.db],
};
const $announcementsRepository: Provider = {
provide: DI.announcementsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAnnouncement),
+ useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository<MiAnnouncement>),
inject: [DI.db],
};
const $announcementReadsRepository: Provider = {
provide: DI.announcementReadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead),
+ useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead).extend(miRepository as MiRepository<MiAnnouncementRead>),
inject: [DI.db],
};
const $appsRepository: Provider = {
provide: DI.appsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiApp),
+ useFactory: (db: DataSource) => db.getRepository(MiApp).extend(miRepository as MiRepository<MiApp>),
inject: [DI.db],
};
const $avatarDecorationsRepository: Provider = {
provide: DI.avatarDecorationsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration),
+ useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration).extend(miRepository as MiRepository<MiAvatarDecoration>),
inject: [DI.db],
};
const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>),
inject: [DI.db],
};
const $noteThreadMutingsRepository: Provider = {
provide: DI.noteThreadMutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository<MiNoteThreadMuting>),
inject: [DI.db],
};
const $noteReactionsRepository: Provider = {
provide: DI.noteReactionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteReaction),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteReaction).extend(miRepository as MiRepository<MiNoteReaction>),
inject: [DI.db],
};
const $noteUnreadsRepository: Provider = {
provide: DI.noteUnreadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteUnread),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository<MiNoteUnread>),
inject: [DI.db],
};
const $pollsRepository: Provider = {
provide: DI.pollsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPoll),
+ useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>),
inject: [DI.db],
};
const $pollVotesRepository: Provider = {
provide: DI.pollVotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPollVote),
+ useFactory: (db: DataSource) => db.getRepository(MiPollVote).extend(miRepository as MiRepository<MiPollVote>),
inject: [DI.db],
};
const $userProfilesRepository: Provider = {
provide: DI.userProfilesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserProfile),
+ useFactory: (db: DataSource) => db.getRepository(MiUserProfile).extend(miRepository as MiRepository<MiUserProfile>),
inject: [DI.db],
};
const $userKeypairsRepository: Provider = {
provide: DI.userKeypairsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserKeypair),
+ useFactory: (db: DataSource) => db.getRepository(MiUserKeypair).extend(miRepository as MiRepository<MiUserKeypair>),
inject: [DI.db],
};
const $userPendingsRepository: Provider = {
provide: DI.userPendingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserPending),
+ useFactory: (db: DataSource) => db.getRepository(MiUserPending).extend(miRepository as MiRepository<MiUserPending>),
inject: [DI.db],
};
const $userSecurityKeysRepository: Provider = {
provide: DI.userSecurityKeysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey),
+ useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey).extend(miRepository as MiRepository<MiUserSecurityKey>),
inject: [DI.db],
};
const $userPublickeysRepository: Provider = {
provide: DI.userPublickeysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserPublickey),
+ useFactory: (db: DataSource) => db.getRepository(MiUserPublickey).extend(miRepository as MiRepository<MiUserPublickey>),
inject: [DI.db],
};
const $userListsRepository: Provider = {
provide: DI.userListsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserList),
+ useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository<MiUserList>),
inject: [DI.db],
};
const $userListFavoritesRepository: Provider = {
provide: DI.userListFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite).extend(miRepository as MiRepository<MiUserListFavorite>),
inject: [DI.db],
};
const $userListMembershipsRepository: Provider = {
provide: DI.userListMembershipsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
+ useFactory: (db: DataSource) => db.getRepository(MiUserListMembership).extend(miRepository as MiRepository<MiUserListMembership>),
inject: [DI.db],
};
const $userNotePiningsRepository: Provider = {
provide: DI.userNotePiningsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserNotePining),
+ useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository<MiUserNotePining>),
inject: [DI.db],
};
const $userIpsRepository: Provider = {
provide: DI.userIpsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserIp),
+ useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository<MiUserIp>),
inject: [DI.db],
};
const $usedUsernamesRepository: Provider = {
provide: DI.usedUsernamesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUsedUsername),
+ useFactory: (db: DataSource) => db.getRepository(MiUsedUsername).extend(miRepository as MiRepository<MiUsedUsername>),
inject: [DI.db],
};
const $followingsRepository: Provider = {
provide: DI.followingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFollowing),
+ useFactory: (db: DataSource) => db.getRepository(MiFollowing).extend(miRepository as MiRepository<MiFollowing>),
inject: [DI.db],
};
const $followRequestsRepository: Provider = {
provide: DI.followRequestsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFollowRequest),
+ useFactory: (db: DataSource) => db.getRepository(MiFollowRequest).extend(miRepository as MiRepository<MiFollowRequest>),
inject: [DI.db],
};
const $instancesRepository: Provider = {
provide: DI.instancesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiInstance),
+ useFactory: (db: DataSource) => db.getRepository(MiInstance).extend(miRepository as MiRepository<MiInstance>),
inject: [DI.db],
};
const $emojisRepository: Provider = {
provide: DI.emojisRepository,
- useFactory: (db: DataSource) => db.getRepository(MiEmoji),
+ useFactory: (db: DataSource) => db.getRepository(MiEmoji).extend(miRepository as MiRepository<MiEmoji>),
inject: [DI.db],
};
const $driveFilesRepository: Provider = {
provide: DI.driveFilesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiDriveFile),
+ useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository<MiDriveFile>),
inject: [DI.db],
};
const $driveFoldersRepository: Provider = {
provide: DI.driveFoldersRepository,
- useFactory: (db: DataSource) => db.getRepository(MiDriveFolder),
+ useFactory: (db: DataSource) => db.getRepository(MiDriveFolder).extend(miRepository as MiRepository<MiDriveFolder>),
inject: [DI.db],
};
const $metasRepository: Provider = {
provide: DI.metasRepository,
- useFactory: (db: DataSource) => db.getRepository(MiMeta),
+ useFactory: (db: DataSource) => db.getRepository(MiMeta).extend(miRepository as MiRepository<MiMeta>),
inject: [DI.db],
};
const $mutingsRepository: Provider = {
provide: DI.mutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiMuting).extend(miRepository as MiRepository<MiMuting>),
inject: [DI.db],
};
const $renoteMutingsRepository: Provider = {
provide: DI.renoteMutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting).extend(miRepository as MiRepository<MiRenoteMuting>),
inject: [DI.db],
};
const $blockingsRepository: Provider = {
provide: DI.blockingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiBlocking),
+ useFactory: (db: DataSource) => db.getRepository(MiBlocking).extend(miRepository as MiRepository<MiBlocking>),
inject: [DI.db],
};
const $swSubscriptionsRepository: Provider = {
provide: DI.swSubscriptionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiSwSubscription),
+ useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository<MiSwSubscription>),
inject: [DI.db],
};
const $hashtagsRepository: Provider = {
provide: DI.hashtagsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiHashtag),
+ useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository<MiHashtag>),
inject: [DI.db],
};
const $abuseUserReportsRepository: Provider = {
provide: DI.abuseUserReportsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport),
+ useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport).extend(miRepository as MiRepository<MiAbuseUserReport>),
inject: [DI.db],
};
const $registrationTicketsRepository: Provider = {
provide: DI.registrationTicketsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket),
+ useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>),
inject: [DI.db],
};
const $authSessionsRepository: Provider = {
provide: DI.authSessionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAuthSession),
+ useFactory: (db: DataSource) => db.getRepository(MiAuthSession).extend(miRepository as MiRepository<MiAuthSession>),
inject: [DI.db],
};
const $accessTokensRepository: Provider = {
provide: DI.accessTokensRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAccessToken),
+ useFactory: (db: DataSource) => db.getRepository(MiAccessToken).extend(miRepository as MiRepository<MiAccessToken>),
inject: [DI.db],
};
const $signinsRepository: Provider = {
provide: DI.signinsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiSignin),
+ useFactory: (db: DataSource) => db.getRepository(MiSignin).extend(miRepository as MiRepository<MiSignin>),
inject: [DI.db],
};
const $pagesRepository: Provider = {
provide: DI.pagesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPage),
+ useFactory: (db: DataSource) => db.getRepository(MiPage).extend(miRepository as MiRepository<MiPage>),
inject: [DI.db],
};
const $pageLikesRepository: Provider = {
provide: DI.pageLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPageLike),
+ useFactory: (db: DataSource) => db.getRepository(MiPageLike).extend(miRepository as MiRepository<MiPageLike>),
inject: [DI.db],
};
const $galleryPostsRepository: Provider = {
provide: DI.galleryPostsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiGalleryPost),
+ useFactory: (db: DataSource) => db.getRepository(MiGalleryPost).extend(miRepository as MiRepository<MiGalleryPost>),
inject: [DI.db],
};
const $galleryLikesRepository: Provider = {
provide: DI.galleryLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiGalleryLike),
+ useFactory: (db: DataSource) => db.getRepository(MiGalleryLike).extend(miRepository as MiRepository<MiGalleryLike>),
inject: [DI.db],
};
const $moderationLogsRepository: Provider = {
provide: DI.moderationLogsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiModerationLog),
+ useFactory: (db: DataSource) => db.getRepository(MiModerationLog).extend(miRepository as MiRepository<MiModerationLog>),
inject: [DI.db],
};
const $clipsRepository: Provider = {
provide: DI.clipsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClip),
+ useFactory: (db: DataSource) => db.getRepository(MiClip).extend(miRepository as MiRepository<MiClip>),
inject: [DI.db],
};
const $clipNotesRepository: Provider = {
provide: DI.clipNotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClipNote),
+ useFactory: (db: DataSource) => db.getRepository(MiClipNote).extend(miRepository as MiRepository<MiClipNote>),
inject: [DI.db],
};
const $clipFavoritesRepository: Provider = {
provide: DI.clipFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClipFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiClipFavorite).extend(miRepository as MiRepository<MiClipFavorite>),
inject: [DI.db],
};
const $antennasRepository: Provider = {
provide: DI.antennasRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAntenna),
+ useFactory: (db: DataSource) => db.getRepository(MiAntenna).extend(miRepository as MiRepository<MiAntenna>),
inject: [DI.db],
};
const $promoNotesRepository: Provider = {
provide: DI.promoNotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPromoNote),
+ useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository<MiPromoNote>),
inject: [DI.db],
};
const $promoReadsRepository: Provider = {
provide: DI.promoReadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPromoRead),
+ useFactory: (db: DataSource) => db.getRepository(MiPromoRead).extend(miRepository as MiRepository<MiPromoRead>),
inject: [DI.db],
};
const $relaysRepository: Provider = {
provide: DI.relaysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRelay),
+ useFactory: (db: DataSource) => db.getRepository(MiRelay).extend(miRepository as MiRepository<MiRelay>),
inject: [DI.db],
};
const $channelsRepository: Provider = {
provide: DI.channelsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannel),
+ useFactory: (db: DataSource) => db.getRepository(MiChannel).extend(miRepository as MiRepository<MiChannel>),
inject: [DI.db],
};
const $channelFollowingsRepository: Provider = {
provide: DI.channelFollowingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing),
+ useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing).extend(miRepository as MiRepository<MiChannelFollowing>),
inject: [DI.db],
};
const $channelFavoritesRepository: Provider = {
provide: DI.channelFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite).extend(miRepository as MiRepository<MiChannelFavorite>),
inject: [DI.db],
};
const $registryItemsRepository: Provider = {
provide: DI.registryItemsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRegistryItem),
+ useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository<MiRegistryItem>),
inject: [DI.db],
};
const $webhooksRepository: Provider = {
provide: DI.webhooksRepository,
- useFactory: (db: DataSource) => db.getRepository(MiWebhook),
+ useFactory: (db: DataSource) => db.getRepository(MiWebhook).extend(miRepository as MiRepository<MiWebhook>),
inject: [DI.db],
};
const $adsRepository: Provider = {
provide: DI.adsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAd),
+ useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>),
inject: [DI.db],
};
const $passwordResetRequestsRepository: Provider = {
provide: DI.passwordResetRequestsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest),
+ useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest).extend(miRepository as MiRepository<MiPasswordResetRequest>),
inject: [DI.db],
};
const $retentionAggregationsRepository: Provider = {
provide: DI.retentionAggregationsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation),
+ useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation).extend(miRepository as MiRepository<MiRetentionAggregation>),
inject: [DI.db],
};
const $flashsRepository: Provider = {
provide: DI.flashsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFlash),
+ useFactory: (db: DataSource) => db.getRepository(MiFlash).extend(miRepository as MiRepository<MiFlash>),
inject: [DI.db],
};
const $flashLikesRepository: Provider = {
provide: DI.flashLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFlashLike),
+ useFactory: (db: DataSource) => db.getRepository(MiFlashLike).extend(miRepository as MiRepository<MiFlashLike>),
inject: [DI.db],
};
const $rolesRepository: Provider = {
provide: DI.rolesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRole),
+ useFactory: (db: DataSource) => db.getRepository(MiRole).extend(miRepository as MiRepository<MiRole>),
inject: [DI.db],
};
const $roleAssignmentsRepository: Provider = {
provide: DI.roleAssignmentsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment),
+ useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment).extend(miRepository as MiRepository<MiRoleAssignment>),
inject: [DI.db],
};
const $userMemosRepository: Provider = {
provide: DI.userMemosRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserMemo),
+ useFactory: (db: DataSource) => db.getRepository(MiUserMemo).extend(miRepository as MiRepository<MiUserMemo>),
inject: [DI.db],
};
const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
+ useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
inject: [DI.db],
};
const $reversiGamesRepository: Provider = {
provide: DI.reversiGamesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiReversiGame),
+ useFactory: (db: DataSource) => db.getRepository(MiReversiGame).extend(miRepository as MiRepository<MiReversiGame>),
inject: [DI.db],
};
diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts
index c03335dd63..6b29a0ce8c 100644
--- a/packages/backend/src/models/ReversiGame.ts
+++ b/packages/backend/src/models/ReversiGame.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts
index 058abe3118..a173971b2c 100644
--- a/packages/backend/src/models/Role.ts
+++ b/packages/backend/src/models/Role.ts
@@ -6,69 +6,149 @@
import { Entity, Column, PrimaryColumn } from 'typeorm';
import { id } from './util/id.js';
+/**
+ * ~かつ~
+ * 複数の条件を同時に満たす場合のみ成立とする
+ */
type CondFormulaValueAnd = {
type: 'and';
values: RoleCondFormulaValue[];
};
+/**
+ * ~または~
+ * 複数の条件のうち、いずれかを満たす場合のみ成立とする
+ */
type CondFormulaValueOr = {
type: 'or';
values: RoleCondFormulaValue[];
};
+/**
+ * ~ではない
+ * 条件を満たさない場合のみ成立とする
+ */
type CondFormulaValueNot = {
type: 'not';
value: RoleCondFormulaValue;
};
+/**
+ * ローカルユーザーのみ成立とする
+ */
type CondFormulaValueIsLocal = {
type: 'isLocal';
};
+/**
+ * リモートユーザーのみ成立とする
+ */
type CondFormulaValueIsRemote = {
type: 'isRemote';
};
+/**
+ * 既に指定のマニュアルロールにアサインされている場合のみ成立とする
+ */
type CondFormulaValueRoleAssignedTo = {
type: 'roleAssignedTo';
roleId: string;
};
+/**
+ * サスペンド済みアカウントの場合のみ成立とする
+ */
+type CondFormulaValueIsSuspended = {
+ type: 'isSuspended';
+};
+
+/**
+ * 鍵アカウントの場合のみ成立とする
+ */
+type CondFormulaValueIsLocked = {
+ type: 'isLocked';
+};
+
+/**
+ * botアカウントの場合のみ成立とする
+ */
+type CondFormulaValueIsBot = {
+ type: 'isBot';
+};
+
+/**
+ * 猫アカウントの場合のみ成立とする
+ */
+type CondFormulaValueIsCat = {
+ type: 'isCat';
+};
+
+/**
+ * 「ユーザを見つけやすくする」が有効なアカウントの場合のみ成立とする
+ */
+type CondFormulaValueIsExplorable = {
+ type: 'isExplorable';
+};
+
+/**
+ * ユーザが作成されてから指定期間経過した場合のみ成立とする
+ */
type CondFormulaValueCreatedLessThan = {
type: 'createdLessThan';
sec: number;
};
+/**
+ * ユーザが作成されてから指定期間経っていない場合のみ成立とする
+ */
type CondFormulaValueCreatedMoreThan = {
type: 'createdMoreThan';
sec: number;
};
+/**
+ * フォロワー数が指定値以下の場合のみ成立とする
+ */
type CondFormulaValueFollowersLessThanOrEq = {
type: 'followersLessThanOrEq';
value: number;
};
+/**
+ * フォロワー数が指定値以上の場合のみ成立とする
+ */
type CondFormulaValueFollowersMoreThanOrEq = {
type: 'followersMoreThanOrEq';
value: number;
};
+/**
+ * フォロー数が指定値以下の場合のみ成立とする
+ */
type CondFormulaValueFollowingLessThanOrEq = {
type: 'followingLessThanOrEq';
value: number;
};
+/**
+ * フォロー数が指定値以上の場合のみ成立とする
+ */
type CondFormulaValueFollowingMoreThanOrEq = {
type: 'followingMoreThanOrEq';
value: number;
};
+/**
+ * 投稿数が指定値以下の場合のみ成立とする
+ */
type CondFormulaValueNotesLessThanOrEq = {
type: 'notesLessThanOrEq';
value: number;
};
+/**
+ * 投稿数が指定値以上の場合のみ成立とする
+ */
type CondFormulaValueNotesMoreThanOrEq = {
type: 'notesMoreThanOrEq';
value: number;
@@ -80,6 +160,11 @@ export type RoleCondFormulaValue = { id: string } & (
CondFormulaValueNot |
CondFormulaValueIsLocal |
CondFormulaValueIsRemote |
+ CondFormulaValueIsSuspended |
+ CondFormulaValueIsLocked |
+ CondFormulaValueIsBot |
+ CondFormulaValueIsCat |
+ CondFormulaValueIsExplorable |
CondFormulaValueRoleAssignedTo |
CondFormulaValueCreatedLessThan |
CondFormulaValueCreatedMoreThan |
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 43d42d80dd..2e6a41586e 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -3,6 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm';
+import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
+import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
+import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
+import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
+import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
+import { OrmUtils } from 'typeorm/util/OrmUtils.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
@@ -70,8 +77,70 @@ import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
+import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
-import type { Repository } from 'typeorm';
+export interface MiRepository<T extends ObjectLiteral> {
+ createTableColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
+ createTableColumnNamesWithPrimaryKey(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
+ insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
+ selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
+}
+
+export const miRepository = {
+ createTableColumnNames(queryBuilder) {
+ // @ts-expect-error -- protected
+ const insertedColumns = queryBuilder.getInsertedColumns();
+ if (insertedColumns.length) {
+ return insertedColumns.map(column => column.databaseName);
+ }
+ if (!queryBuilder.expressionMap.mainAlias?.hasMetadata && !queryBuilder.expressionMap.insertColumns.length) {
+ // @ts-expect-error -- protected
+ const valueSets = queryBuilder.getValueSets();
+ if (valueSets.length === 1) {
+ return Object.keys(valueSets[0]);
+ }
+ }
+ return queryBuilder.expressionMap.insertColumns;
+ },
+ createTableColumnNamesWithPrimaryKey(queryBuilder) {
+ const columnNames = this.createTableColumnNames(queryBuilder);
+ if (!columnNames.includes('id')) {
+ columnNames.unshift('id');
+ }
+ return columnNames;
+ },
+ async insertOne(entity, findOptions?) {
+ const queryBuilder = this.createQueryBuilder().insert().values(entity);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const mainAlias = queryBuilder.expressionMap.mainAlias!;
+ const name = mainAlias.name;
+ mainAlias.name = 't';
+ const columnNames = this.createTableColumnNamesWithPrimaryKey(queryBuilder);
+ queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
+ const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ builder.expressionMap.mainAlias!.tablePath = 'cte';
+ this.selectAliasColumnNames(queryBuilder, builder);
+ if (findOptions) {
+ builder.setFindOptions(findOptions);
+ }
+ const raw = await builder.execute();
+ mainAlias.name = name;
+ const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw);
+ const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw);
+ const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias);
+ return result[0];
+ },
+ selectAliasColumnNames(queryBuilder, builder) {
+ let selectOrAddSelect = (selection: string, selectionAliasName?: string) => {
+ selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
+ return builder.select(selection, selectionAliasName);
+ };
+ for (const columnName of this.createTableColumnNamesWithPrimaryKey(queryBuilder)) {
+ selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
+ }
+ },
+} satisfies MiRepository<ObjectLiteral>;
export {
MiAbuseUserReport,
@@ -143,70 +212,70 @@ export {
MiReversiGame,
};
-export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
-export type AccessTokensRepository = Repository<MiAccessToken>;
-export type AdsRepository = Repository<MiAd>;
-export type AnnouncementsRepository = Repository<MiAnnouncement>;
-export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>;
-export type AntennasRepository = Repository<MiAntenna>;
-export type AppsRepository = Repository<MiApp>;
-export type AvatarDecorationsRepository = Repository<MiAvatarDecoration>;
-export type AuthSessionsRepository = Repository<MiAuthSession>;
-export type BlockingsRepository = Repository<MiBlocking>;
-export type ChannelFollowingsRepository = Repository<MiChannelFollowing>;
-export type ChannelFavoritesRepository = Repository<MiChannelFavorite>;
-export type ClipsRepository = Repository<MiClip>;
-export type ClipNotesRepository = Repository<MiClipNote>;
-export type ClipFavoritesRepository = Repository<MiClipFavorite>;
-export type DriveFilesRepository = Repository<MiDriveFile>;
-export type DriveFoldersRepository = Repository<MiDriveFolder>;
-export type EmojisRepository = Repository<MiEmoji>;
-export type FollowingsRepository = Repository<MiFollowing>;
-export type FollowRequestsRepository = Repository<MiFollowRequest>;
-export type GalleryLikesRepository = Repository<MiGalleryLike>;
-export type GalleryPostsRepository = Repository<MiGalleryPost>;
-export type HashtagsRepository = Repository<MiHashtag>;
-export type InstancesRepository = Repository<MiInstance>;
-export type MetasRepository = Repository<MiMeta>;
-export type ModerationLogsRepository = Repository<MiModerationLog>;
-export type MutingsRepository = Repository<MiMuting>;
-export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
-export type NotesRepository = Repository<MiNote>;
-export type NoteFavoritesRepository = Repository<MiNoteFavorite>;
-export type NoteReactionsRepository = Repository<MiNoteReaction>;
-export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting>;
-export type NoteUnreadsRepository = Repository<MiNoteUnread>;
-export type PagesRepository = Repository<MiPage>;
-export type PageLikesRepository = Repository<MiPageLike>;
-export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest>;
-export type PollsRepository = Repository<MiPoll>;
-export type PollVotesRepository = Repository<MiPollVote>;
-export type PromoNotesRepository = Repository<MiPromoNote>;
-export type PromoReadsRepository = Repository<MiPromoRead>;
-export type RegistrationTicketsRepository = Repository<MiRegistrationTicket>;
-export type RegistryItemsRepository = Repository<MiRegistryItem>;
-export type RelaysRepository = Repository<MiRelay>;
-export type SigninsRepository = Repository<MiSignin>;
-export type SwSubscriptionsRepository = Repository<MiSwSubscription>;
-export type UsedUsernamesRepository = Repository<MiUsedUsername>;
-export type UsersRepository = Repository<MiUser>;
-export type UserIpsRepository = Repository<MiUserIp>;
-export type UserKeypairsRepository = Repository<MiUserKeypair>;
-export type UserListsRepository = Repository<MiUserList>;
-export type UserListFavoritesRepository = Repository<MiUserListFavorite>;
-export type UserListMembershipsRepository = Repository<MiUserListMembership>;
-export type UserNotePiningsRepository = Repository<MiUserNotePining>;
-export type UserPendingsRepository = Repository<MiUserPending>;
-export type UserProfilesRepository = Repository<MiUserProfile>;
-export type UserPublickeysRepository = Repository<MiUserPublickey>;
-export type UserSecurityKeysRepository = Repository<MiUserSecurityKey>;
-export type WebhooksRepository = Repository<MiWebhook>;
-export type ChannelsRepository = Repository<MiChannel>;
-export type RetentionAggregationsRepository = Repository<MiRetentionAggregation>;
-export type RolesRepository = Repository<MiRole>;
-export type RoleAssignmentsRepository = Repository<MiRoleAssignment>;
-export type FlashsRepository = Repository<MiFlash>;
-export type FlashLikesRepository = Repository<MiFlashLike>;
-export type UserMemoRepository = Repository<MiUserMemo>;
-export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;
-export type ReversiGamesRepository = Repository<MiReversiGame>;
+export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
+export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
+export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
+export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
+export type AnnouncementReadsRepository = Repository<MiAnnouncementRead> & MiRepository<MiAnnouncementRead>;
+export type AntennasRepository = Repository<MiAntenna> & MiRepository<MiAntenna>;
+export type AppsRepository = Repository<MiApp> & MiRepository<MiApp>;
+export type AvatarDecorationsRepository = Repository<MiAvatarDecoration> & MiRepository<MiAvatarDecoration>;
+export type AuthSessionsRepository = Repository<MiAuthSession> & MiRepository<MiAuthSession>;
+export type BlockingsRepository = Repository<MiBlocking> & MiRepository<MiBlocking>;
+export type ChannelFollowingsRepository = Repository<MiChannelFollowing> & MiRepository<MiChannelFollowing>;
+export type ChannelFavoritesRepository = Repository<MiChannelFavorite> & MiRepository<MiChannelFavorite>;
+export type ClipsRepository = Repository<MiClip> & MiRepository<MiClip>;
+export type ClipNotesRepository = Repository<MiClipNote> & MiRepository<MiClipNote>;
+export type ClipFavoritesRepository = Repository<MiClipFavorite> & MiRepository<MiClipFavorite>;
+export type DriveFilesRepository = Repository<MiDriveFile> & MiRepository<MiDriveFile>;
+export type DriveFoldersRepository = Repository<MiDriveFolder> & MiRepository<MiDriveFolder>;
+export type EmojisRepository = Repository<MiEmoji> & MiRepository<MiEmoji>;
+export type FollowingsRepository = Repository<MiFollowing> & MiRepository<MiFollowing>;
+export type FollowRequestsRepository = Repository<MiFollowRequest> & MiRepository<MiFollowRequest>;
+export type GalleryLikesRepository = Repository<MiGalleryLike> & MiRepository<MiGalleryLike>;
+export type GalleryPostsRepository = Repository<MiGalleryPost> & MiRepository<MiGalleryPost>;
+export type HashtagsRepository = Repository<MiHashtag> & MiRepository<MiHashtag>;
+export type InstancesRepository = Repository<MiInstance> & MiRepository<MiInstance>;
+export type MetasRepository = Repository<MiMeta> & MiRepository<MiMeta>;
+export type ModerationLogsRepository = Repository<MiModerationLog> & MiRepository<MiModerationLog>;
+export type MutingsRepository = Repository<MiMuting> & MiRepository<MiMuting>;
+export type RenoteMutingsRepository = Repository<MiRenoteMuting> & MiRepository<MiRenoteMuting>;
+export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
+export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
+export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
+export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
+export type NoteUnreadsRepository = Repository<MiNoteUnread> & MiRepository<MiNoteUnread>;
+export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
+export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
+export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>;
+export type PollsRepository = Repository<MiPoll> & MiRepository<MiPoll>;
+export type PollVotesRepository = Repository<MiPollVote> & MiRepository<MiPollVote>;
+export type PromoNotesRepository = Repository<MiPromoNote> & MiRepository<MiPromoNote>;
+export type PromoReadsRepository = Repository<MiPromoRead> & MiRepository<MiPromoRead>;
+export type RegistrationTicketsRepository = Repository<MiRegistrationTicket> & MiRepository<MiRegistrationTicket>;
+export type RegistryItemsRepository = Repository<MiRegistryItem> & MiRepository<MiRegistryItem>;
+export type RelaysRepository = Repository<MiRelay> & MiRepository<MiRelay>;
+export type SigninsRepository = Repository<MiSignin> & MiRepository<MiSignin>;
+export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>;
+export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>;
+export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>;
+export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>;
+export type UserKeypairsRepository = Repository<MiUserKeypair> & MiRepository<MiUserKeypair>;
+export type UserListsRepository = Repository<MiUserList> & MiRepository<MiUserList>;
+export type UserListFavoritesRepository = Repository<MiUserListFavorite> & MiRepository<MiUserListFavorite>;
+export type UserListMembershipsRepository = Repository<MiUserListMembership> & MiRepository<MiUserListMembership>;
+export type UserNotePiningsRepository = Repository<MiUserNotePining> & MiRepository<MiUserNotePining>;
+export type UserPendingsRepository = Repository<MiUserPending> & MiRepository<MiUserPending>;
+export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<MiUserProfile>;
+export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>;
+export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>;
+export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>;
+export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>;
+export type RetentionAggregationsRepository = Repository<MiRetentionAggregation> & MiRepository<MiRetentionAggregation>;
+export type RolesRepository = Repository<MiRole> & MiRepository<MiRole>;
+export type RoleAssignmentsRepository = Repository<MiRoleAssignment> & MiRepository<MiRoleAssignment>;
+export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>;
+export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>;
+export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
+export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
+export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts
index 74622b6193..b5b9a5b42c 100644
--- a/packages/backend/src/models/json-schema/antenna.ts
+++ b/packages/backend/src/models/json-schema/antenna.ts
@@ -72,9 +72,10 @@ export const packedAntennaSchema = {
optional: false, nullable: false,
default: false,
},
- notify: {
+ excludeBots: {
type: 'boolean',
optional: false, nullable: false,
+ default: false,
},
withReplies: {
type: 'boolean',
@@ -94,5 +95,10 @@ export const packedAntennaSchema = {
optional: false, nullable: false,
default: false,
},
+ notify: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ default: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/clip.ts b/packages/backend/src/models/json-schema/clip.ts
index ca4886c978..c4e7055cd8 100644
--- a/packages/backend/src/models/json-schema/clip.ts
+++ b/packages/backend/src/models/json-schema/clip.ts
@@ -52,5 +52,9 @@ export const packedClipSchema = {
type: 'boolean',
optional: true, nullable: false,
},
+ notesCount: {
+ type: 'integer',
+ optional: true, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts
index 42d98fe523..ed40d405c6 100644
--- a/packages/backend/src/models/json-schema/federation-instance.ts
+++ b/packages/backend/src/models/json-schema/federation-instance.ts
@@ -45,6 +45,11 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ suspensionState: {
+ type: 'string',
+ nullable: false, optional: false,
+ enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
+ },
isBlocked: {
type: 'boolean',
optional: false, nullable: false,
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 17789f3b46..e7bc6356e5 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -207,6 +207,10 @@ export const packedMetaLiteSchema = {
type: 'string',
optional: false, nullable: false,
},
+ enableUrlPreview: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
backgroundImageUrl: {
type: 'string',
optional: false, nullable: true,
@@ -223,6 +227,10 @@ export const packedMetaLiteSchema = {
type: 'string',
optional: false, nullable: true,
},
+ inquiryUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
serverRules: {
type: 'array',
optional: false, nullable: false,
diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts
index bb4ccc7ee4..2641161c8b 100644
--- a/packages/backend/src/models/json-schema/note.ts
+++ b/packages/backend/src/models/json-schema/note.ts
@@ -223,6 +223,10 @@ export const packedNoteSchema = {
}],
},
},
+ reactionCount: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
renoteCount: {
type: 'number',
optional: false, nullable: false,
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index c770250503..d9987a70c3 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -57,6 +57,20 @@ export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = {
},
} as const;
+export const packedRoleCondFormulaValueUserSettingBooleanSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string', optional: false,
+ },
+ type: {
+ type: 'string',
+ nullable: false, optional: false,
+ enum: ['isSuspended', 'isLocked', 'isBot', 'isCat', 'isExplorable'],
+ },
+ },
+} as const;
+
export const packedRoleCondFormulaValueAssignedRoleSchema = {
type: 'object',
properties: {
@@ -136,6 +150,9 @@ export const packedRoleCondFormulaValueSchema = {
ref: 'RoleCondFormulaValueIsLocalOrRemote',
},
{
+ ref: 'RoleCondFormulaValueUserSettingBooleanSchema',
+ },
+ {
ref: 'RoleCondFormulaValueAssignedRole',
},
{
diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts
index d27d2490c5..45732a742b 100644
--- a/packages/backend/src/models/json-schema/signin.ts
+++ b/packages/backend/src/models/json-schema/signin.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
export const packedSigninSchema = {
type: 'object',
properties: {
diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
index 917de8b72c..728fc9e72b 100644
--- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
@@ -63,7 +63,7 @@ export class CleanRemoteFilesProcessorService {
isLink: false,
});
- job.updateProgress(deletedCount / total);
+ job.updateProgress(100 / total * deletedCount);
}
this.logger.succ('All cached remote files has been deleted.');
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index 5fed070929..b73195afc3 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Bull from 'bullmq';
+import { Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { InstancesRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
@@ -62,7 +63,7 @@ export class DeliverProcessorService {
if (suspendedHosts == null) {
suspendedHosts = await this.instancesRepository.find({
where: {
- isSuspended: true,
+ suspensionState: Not('none'),
},
});
this.suspendedHostsCache.set(suspendedHosts);
@@ -79,6 +80,7 @@ export class DeliverProcessorService {
if (i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: false,
+ notRespondingSince: null,
});
}
@@ -98,7 +100,15 @@ export class DeliverProcessorService {
if (!i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: true,
+ notRespondingSince: new Date(),
});
+ } else if (i.notRespondingSince) {
+ // 1週間以上不通ならサスペンド
+ if (i.suspensionState === 'none' && i.notRespondingSince.getTime() <= Date.now() - 1000 * 60 * 60 * 24 * 7) {
+ this.federatedInstanceService.update(i.id, {
+ suspensionState: 'autoSuspendedForNotResponding',
+ });
+ }
}
this.apRequestChart.deliverFail();
@@ -116,7 +126,7 @@ export class DeliverProcessorService {
if (job.data.isSharedInbox && res.statusCode === 410) {
this.federatedInstanceService.fetch(host).then(i => {
this.federatedInstanceService.update(i.id, {
- isSuspended: true,
+ suspensionState: 'goneSuspended',
});
});
throw new Bull.UnrecoverableError(`${host} is gone`);
diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
index af48bad417..88c4ea29c0 100644
--- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
@@ -81,9 +81,9 @@ export class ExportAntennasProcessorService {
}) : null,
caseSensitive: antenna.caseSensitive,
localOnly: antenna.localOnly,
+ excludeBots: antenna.excludeBots,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
- notify: antenna.notify,
}));
if (antennas.length - 1 !== index) {
write(', ');
diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
index 951b560597..9c033b73e2 100644
--- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
@@ -44,11 +44,11 @@ const validate = new Ajv().compile({
} },
caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
+ excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
- notify: { type: 'boolean' },
},
- required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
+ required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
});
@Injectable()
@@ -76,7 +76,7 @@ export class ImportAntennasProcessorService {
this.logger.warn('Validation Failed');
continue;
}
- const result = await this.antennasRepository.insert({
+ const result = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: job.data.user.id,
@@ -88,10 +88,10 @@ export class ImportAntennasProcessorService {
users: (antenna.src === 'list' && antenna.userListAccts !== null ? antenna.userListAccts : antenna.users).filter(Boolean),
caseSensitive: antenna.caseSensitive,
localOnly: antenna.localOnly,
+ excludeBots: antenna.excludeBots,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
- notify: antenna.notify,
- }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.logger.succ('Antenna created: ' + result.id);
this.globalEventService.publishInternalEvent('antennaCreated', result);
}
diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
index a5992c28c8..db9255b35d 100644
--- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
@@ -79,11 +79,11 @@ export class ImportUserListsProcessorService {
});
if (list == null) {
- list = await this.userListsRepository.insert({
+ list = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: user.id,
name: listName,
- }).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
+ });
}
let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 3addead058..fa7009f8f5 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -15,13 +15,14 @@ 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/activitypub/type.js';
+import type { IActivity } from '@/core/activitypub/type.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiUserPublickey } from '@/models/UserPublickey.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
-import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
+import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -38,7 +39,7 @@ export class InboxProcessorService {
private apInboxService: ApInboxService,
private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService,
- private ldSignatureService: LdSignatureService,
+ private jsonLdService: JsonLdService,
private apPersonService: ApPersonService,
private apDbResolverService: ApDbResolverService,
private instanceChart: InstanceChart,
@@ -52,7 +53,7 @@ export class InboxProcessorService {
@bindThis
public async process(job: Bull.Job<InboxJobData>): Promise<string> {
const signature = job.data.signature; // HTTP-signature
- const activity = job.data.activity;
+ let activity = job.data.activity;
//#region Log
const info = Object.assign({}, activity);
@@ -110,20 +111,21 @@ export class InboxProcessorService {
// また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
- if (activity.signature) {
- if (activity.signature.type !== 'RsaSignature2017') {
- throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`);
+ const ldSignature = activity.signature;
+ if (ldSignature) {
+ if (ldSignature.type !== 'RsaSignature2017') {
+ throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`);
}
- // activity.signature.creator: https://example.oom/users/user#main-key
+ // ldSignature.creator: https://example.oom/users/user#main-key
// みたいになっててUserを引っ張れば公開キーも入ることを期待する
- if (activity.signature.creator) {
- const candicate = activity.signature.creator.replace(/#.*/, '');
+ if (ldSignature.creator) {
+ const candicate = ldSignature.creator.replace(/#.*/, '');
await this.apPersonService.resolvePerson(candicate).catch(() => null);
}
// keyIdからLD-Signatureのユーザーを取得
- authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator);
+ authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator);
if (authUser == null) {
throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
}
@@ -132,13 +134,31 @@ export class InboxProcessorService {
throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
}
+ const jsonLd = this.jsonLdService.use();
+
// LD-Signature検証
- const ldSignature = this.ldSignatureService.use();
- const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
+ const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
if (!verified) {
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
}
+ // アクティビティを正規化
+ delete activity.signature;
+ try {
+ activity = await jsonLd.compact(activity) as IActivity;
+ } catch (e) {
+ throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`);
+ }
+ // TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする
+ // https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29
+ activity.signature = ldSignature;
+
+ //#region Log
+ const compactedInfo = Object.assign({}, activity);
+ delete compactedInfo['@context'];
+ this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`);
+ //#endregion
+
// もう一度actorチェック
if (authUser.user.uri !== activity.actor) {
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
@@ -168,6 +188,8 @@ export class InboxProcessorService {
this.federatedInstanceService.update(i.id, {
latestRequestReceivedAt: new Date(),
isNotResponding: false,
+ // もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
+ suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
});
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
@@ -182,13 +204,22 @@ export class InboxProcessorService {
// アクティビティを処理
try {
- await this.apInboxService.performActivity(authUser.user, activity);
+ const result = await this.apInboxService.performActivity(authUser.user, activity);
+ if (result && !result.startsWith('ok')) {
+ this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`);
+ return result;
+ }
} catch (e) {
if (e instanceof IdentifiableError) {
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
return 'blocked notes with prohibited words';
}
- if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') return 'actor has been suspended';
+ if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') {
+ return 'actor has been suspended';
+ }
+ if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note
+ return e.message;
+ }
}
throw e;
}
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index 60366dd5c2..3255d64621 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -28,7 +28,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
@@ -91,7 +91,7 @@ export class ActivityPubServerService {
*/
@bindThis
private async packActivity(note: MiNote): Promise<any> {
- if (isPureRenote(note)) {
+ if (isRenote(note) && !isQuote(note)) {
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
}
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index f51d7aebca..9db3aa1bfb 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -194,6 +194,7 @@ export class FileServerService {
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
+ reply.code(206);
} else {
image = {
data: fs.createReadStream(file.path),
@@ -213,6 +214,8 @@ export class FileServerService {
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
+ reply.header('Content-Length', file.file.size);
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
@@ -255,6 +258,7 @@ export class FileServerService {
return fs.createReadStream(file.path);
} else {
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
+ reply.header('Content-Length', file.file.size);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
@@ -263,7 +267,6 @@ export class FileServerService {
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
- console.log(end);
if (end > file.file.size) {
end = file.file.size - 1;
}
@@ -433,6 +436,7 @@ export class FileServerService {
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
+ reply.code(206);
} else {
image = {
data: fs.createReadStream(file.path),
@@ -529,6 +533,7 @@ export class FileServerService {
if (!file.storedInternal) {
if (!(file.isLink && file.uri)) return '204';
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
+ file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので
return {
...result,
url: file.uri,
diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts
new file mode 100644
index 0000000000..2c3ed85925
--- /dev/null
+++ b/packages/backend/src/server/HealthServerService.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import { DataSource } from 'typeorm';
+import { bindThis } from '@/decorators.js';
+import { DI } from '@/di-symbols.js';
+import { readyRef } from '@/boot/ready.js';
+import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
+import type { MeiliSearch } from 'meilisearch';
+
+@Injectable()
+export class HealthServerService {
+ constructor(
+ @Inject(DI.redis)
+ private redis: Redis.Redis,
+
+ @Inject(DI.redisForPub)
+ private redisForPub: Redis.Redis,
+
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+
+ @Inject(DI.redisForTimelines)
+ private redisForTimelines: Redis.Redis,
+
+ @Inject(DI.db)
+ private db: DataSource,
+
+ @Inject(DI.meilisearch)
+ private meilisearch: MeiliSearch | null,
+ ) {}
+
+ @bindThis
+ public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
+ fastify.get('/', async (request, reply) => {
+ reply.code(await Promise.all([
+ new Promise<void>((resolve, reject) => readyRef.value ? resolve() : reject()),
+ this.redis.ping(),
+ this.redisForPub.ping(),
+ this.redisForSub.ping(),
+ this.redisForTimelines.ping(),
+ this.db.query('SELECT 1'),
+ ...(this.meilisearch ? [this.meilisearch.health()] : []),
+ ]).then(() => 200, () => 503));
+ reply.header('Cache-Control', 'no-store');
+ });
+
+ done();
+ }
+}
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index c1e5af08c9..cc18997fdc 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -37,12 +37,12 @@ export class NodeinfoServerService {
@bindThis
public getLinks() {
return [{
- rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
- href: this.config.url + nodeinfo2_1path
- }, {
- rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
- href: this.config.url + nodeinfo2_0path,
- }];
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
+ href: this.config.url + nodeinfo2_1path,
+ }, {
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
+ href: this.config.url + nodeinfo2_0path,
+ }];
}
@bindThis
@@ -108,6 +108,7 @@ export class NodeinfoServerService {
langs: meta.langs,
tosUrl: meta.termsOfServiceUrl,
privacyPolicyUrl: meta.privacyPolicyUrl,
+ inquiryUrl: meta.inquiryUrl,
impressumUrl: meta.impressumUrl,
repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl,
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index f43968d236..12d5061985 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -8,6 +8,7 @@ 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 { HealthServerService } from './HealthServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
@@ -55,6 +56,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
ClientServerService,
ClientLoggerService,
FeedService,
+ HealthServerService,
UrlPreviewService,
ActivityPubServerService,
FileServerService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 671dd31eb1..3572f16627 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -18,7 +18,6 @@ import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.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 { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
@@ -29,6 +28,7 @@ import { ApiServerService } from './api/ApiServerService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
import { FileServerService } from './FileServerService.js';
+import { HealthServerService } from './HealthServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
@@ -62,6 +62,7 @@ export class ServerService implements OnApplicationShutdown {
private wellKnownServerService: WellKnownServerService,
private nodeinfoServerService: NodeinfoServerService,
private fileServerService: FileServerService,
+ private healthServerService: HealthServerService,
private clientServerService: ClientServerService,
private globalEventService: GlobalEventService,
private loggerService: LoggerService,
@@ -109,6 +110,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
+ fastify.register(this.healthServerService.createServer, { prefix: '/healthz' });
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path;
@@ -120,12 +122,20 @@ export class ServerService implements OnApplicationShutdown {
return;
}
- const name = path.split('@')[0].replace(/\.webp$/i, '');
- const host = path.split('@')[1]?.replace(/\.webp$/i, '');
+ const emojiPath = path.replace(/\.webp$/i, '');
+ const pathChunks = emojiPath.split('@');
+
+ if (pathChunks.length > 2) {
+ reply.code(400);
+ return;
+ }
+
+ const name = pathChunks.shift();
+ const host = pathChunks.pop();
const emoji = await this.emojisRepository.findOneBy({
// `@.` is the spec of ReactionService.decodeReaction
- host: (host == null || host === '.') ? IsNull() : host,
+ host: (host === undefined || host === '.') ? IsNull() : host,
name: name,
});
@@ -184,9 +194,7 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Cache-Control', 'public, max-age=86400');
if ((await this.metaService.fetch()).enableIdenticonGeneration) {
- const [temp, cleanup] = await createTemp();
- await genIdenticon(request.params.x, fs.createWriteStream(temp));
- return fs.createReadStream(temp).on('close', () => cleanup());
+ return await genIdenticon(request.params.x);
} else {
return reply.redirect('/static-assets/avatar.png');
}
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 9836689872..271ef80554 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
+import * as Sentry from '@sentry/node';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -17,6 +18,7 @@ import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import type { Config } from '@/config.js';
import { ApiError } from './error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
@@ -38,6 +40,9 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
@@ -88,6 +93,48 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
+ #onExecError(ep: IEndpoint, data: any, err: Error): void {
+ if (err instanceof ApiError || err instanceof AuthenticationError) {
+ throw err;
+ } else {
+ const errId = randomUUID();
+ 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,
+ id: errId,
+ },
+ });
+ console.error(err, errId);
+
+ if (this.config.sentryForBackend) {
+ Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ extra: {
+ ep: ep.name,
+ ps: data,
+ e: {
+ message: err.message,
+ code: err.name,
+ stack: err.stack,
+ id: errId,
+ },
+ },
+ });
+ }
+
+ throw new ApiError(null, {
+ e: {
+ message: err.message,
+ code: err.name,
+ id: errId,
+ },
+ });
+ }
+ }
+
@bindThis
public handleRequest(
endpoint: IEndpoint & { exec: any },
@@ -362,31 +409,11 @@ export class ApiCallService implements OnApplicationShutdown {
}
// API invoking
- return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
- if (err instanceof ApiError || err instanceof AuthenticationError) {
- throw err;
- } else {
- const errId = randomUUID();
- 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,
- id: errId,
- },
- });
- console.error(err, errId);
- throw new ApiError(null, {
- e: {
- message: err.message,
- code: err.name,
- id: errId,
- },
- });
- }
- });
+ if (this.config.sentryForBackend) {
+ return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)));
+ } else {
+ return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err));
+ }
}
@bindThis
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index e99244cdd0..4a5935f930 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -137,7 +137,7 @@ export class ApiServerService {
const instances = await this.instancesRepository.find({
select: ['host'],
where: {
- isSuspended: false,
+ suspensionState: 'none',
},
});
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 88d3999eb0..c645f4bcc6 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -83,6 +83,7 @@ import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
+import * as ep___announcements_show from './endpoints/announcements/show.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';
@@ -455,6 +456,7 @@ const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', us
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
+const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.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 };
@@ -831,6 +833,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
$announcements,
+ $announcements_show,
$antennas_create,
$antennas_delete,
$antennas_list,
@@ -1201,6 +1204,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
$announcements,
+ $announcements_show,
$antennas_create,
$antennas_delete,
$antennas_list,
diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts
index 714e56e8c3..70306c3113 100644
--- a/packages/backend/src/server/api/SigninService.ts
+++ b/packages/backend/src/server/api/SigninService.ts
@@ -29,13 +29,13 @@ export class SigninService {
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
setImmediate(async () => {
// Append signin history
- const record = await this.signinsRepository.insert({
+ const record = await this.signinsRepository.insertOne({
id: this.idService.gen(),
userId: user.id,
ip: request.ip,
headers: request.headers as any,
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
index 546de48e6b..632b0c62bc 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -183,13 +183,13 @@ export class SignupApiService {
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
- const pendingUser = await this.userPendingsRepository.insert({
+ const pendingUser = await this.userPendingsRepository.insertOne({
id: this.idService.gen(),
code,
email: emailAddress!,
username: username,
password: hash,
- }).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
+ });
const link = `${this.config.url}/signup-complete/${code}`;
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index f7e64a7356..a38c62f35a 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -83,6 +83,7 @@ import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
+import * as ep___announcements_show from './endpoints/announcements/show.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';
@@ -453,6 +454,7 @@ const eps = [
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
['admin/roles/users', ep___admin_roles_users],
['announcements', ep___announcements],
+ ['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
['antennas/list', ep___antennas_list],
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 1e7a9fb3ec..955154f4fb 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
- const ad = await this.adsRepository.insert({
+ const ad = await this.adsRepository.insertOne({
id: this.idService.gen(),
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio,
place: ps.place,
memo: ps.memo,
- }).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
+ });
this.moderationLogService.log(me, 'createAd', {
adId: ad.id,
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 0bcdc2a4b8..fed7bfbbde 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
@@ -46,12 +46,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('instance not found');
}
+ const isSuspendedBefore = instance.suspensionState !== 'none';
+ let suspensionState: undefined | 'manuallySuspended' | 'none';
+
+ if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
+ suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
+ }
+
await this.federatedInstanceService.update(instance.id, {
- isSuspended: ps.isSuspended,
+ suspensionState,
moderationNote: ps.moderationNote,
});
- if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
+ if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id,
diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
index 0f551e1ba2..5ecae3161a 100644
--- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
@@ -66,11 +66,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const ticketsPromises = [];
for (let i = 0; i < ps.count; i++) {
- ticketsPromises.push(this.registrationTicketsRepository.insert({
+ ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
- }).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
+ }));
}
const tickets = await Promise.all(ticketsPromises);
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 88c5907bcc..eee02a7123 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -427,6 +427,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ inquiryUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
@@ -434,6 +438,8 @@ export const meta = {
summalyProxy: {
type: 'string',
optional: false, nullable: true,
+ deprecated: true,
+ description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
},
themeColor: {
type: 'string',
@@ -451,6 +457,30 @@ export const meta = {
type: 'string',
optional: false, nullable: false,
},
+ urlPreviewEnabled: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ urlPreviewTimeout: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ urlPreviewMaximumContentLength: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ urlPreviewRequireContentLength: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ urlPreviewUserAgent: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ urlPreviewSummaryProxyUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
},
},
} as const;
@@ -487,6 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
+ inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
@@ -533,7 +564,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
proxyAccountId: instance.proxyAccountId,
- summalyProxy: instance.summalyProxy,
email: instance.email,
smtpSecure: instance.smtpSecure,
smtpHost: instance.smtpHost,
@@ -577,6 +607,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
notesPerOneAd: instance.notesPerOneAd,
+ summalyProxy: instance.urlPreviewSummaryProxyUrl,
+ urlPreviewEnabled: instance.urlPreviewEnabled,
+ urlPreviewTimeout: instance.urlPreviewTimeout,
+ urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength,
+ urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
+ urlPreviewUserAgent: instance.urlPreviewUserAgent,
+ urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
index 45758d4f50..198166bec2 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
@@ -89,10 +89,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
+ const _users = assigns.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
+ .then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(),
- user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
+ user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
expiresAt: assign.expiresAt?.toISOString() ?? null,
})));
});
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 424212ba24..2fef9abbf9 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -16,7 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
- kind: 'read:admin:show-users',
+ kind: 'read:admin:show-user',
res: {
type: 'array',
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 bffceef815..4e28ee6877 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -90,7 +90,6 @@ export const paramDef = {
type: 'string',
},
},
- summalyProxy: { type: 'string', nullable: true },
deeplAuthKey: { type: 'string', nullable: true },
deeplIsPro: { type: 'boolean' },
enableEmail: { type: 'boolean' },
@@ -108,6 +107,7 @@ export const paramDef = {
feedbackUrl: { type: 'string', nullable: true },
impressumUrl: { type: 'string', nullable: true },
privacyPolicyUrl: { type: 'string', nullable: true },
+ inquiryUrl: { type: 'string', nullable: true },
useObjectStorage: { type: 'boolean' },
objectStorageBaseUrl: { type: 'string', nullable: true },
objectStorageBucket: { type: 'string', nullable: true },
@@ -150,6 +150,16 @@ export const paramDef = {
type: 'string',
},
},
+ summalyProxy: {
+ type: 'string', nullable: true,
+ description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
+ },
+ urlPreviewEnabled: { type: 'boolean' },
+ urlPreviewTimeout: { type: 'integer' },
+ urlPreviewMaximumContentLength: { type: 'integer' },
+ urlPreviewRequireContentLength: { type: 'boolean' },
+ urlPreviewUserAgent: { type: 'string', nullable: true },
+ urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
},
required: [],
} as const;
@@ -353,10 +363,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.langs = ps.langs.filter(Boolean);
}
- if (ps.summalyProxy !== undefined) {
- set.summalyProxy = ps.summalyProxy;
- }
-
if (ps.enableEmail !== undefined) {
set.enableEmail = ps.enableEmail;
}
@@ -417,6 +423,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.privacyPolicyUrl = ps.privacyPolicyUrl;
}
+ if (ps.inquiryUrl !== undefined) {
+ set.inquiryUrl = ps.inquiryUrl;
+ }
+
if (ps.useObjectStorage !== undefined) {
set.useObjectStorage = ps.useObjectStorage;
}
@@ -581,6 +591,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.bannedEmailDomains = ps.bannedEmailDomains;
}
+ if (ps.urlPreviewEnabled !== undefined) {
+ set.urlPreviewEnabled = ps.urlPreviewEnabled;
+ }
+
+ if (ps.urlPreviewTimeout !== undefined) {
+ set.urlPreviewTimeout = ps.urlPreviewTimeout;
+ }
+
+ if (ps.urlPreviewMaximumContentLength !== undefined) {
+ set.urlPreviewMaximumContentLength = ps.urlPreviewMaximumContentLength;
+ }
+
+ if (ps.urlPreviewRequireContentLength !== undefined) {
+ set.urlPreviewRequireContentLength = ps.urlPreviewRequireContentLength;
+ }
+
+ if (ps.urlPreviewUserAgent !== undefined) {
+ const value = (ps.urlPreviewUserAgent ?? '').trim();
+ set.urlPreviewUserAgent = value === '' ? null : ps.urlPreviewUserAgent;
+ }
+
+ if (ps.summalyProxy !== undefined || ps.urlPreviewSummaryProxyUrl !== undefined) {
+ const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim();
+ set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
+ }
+
const before = await this.metaService.fetch(true);
await this.metaService.update(set);
diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts
index 3b12f5b62c..ff8dd73605 100644
--- a/packages/backend/src/server/api/endpoints/announcements.ts
+++ b/packages/backend/src/server/api/endpoints/announcements.ts
@@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
-import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { DI } from '@/di-symbols.js';
-import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js';
+import type { AnnouncementsRepository } from '@/models/_.js';
export const meta = {
tags: ['meta'],
@@ -44,11 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
- @Inject(DI.announcementReadsRepository)
- private announcementReadsRepository: AnnouncementReadsRepository,
-
private queryService: QueryService,
- private announcementService: AnnouncementService,
+ private announcementEntityService: AnnouncementEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
@@ -60,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const announcements = await query.limit(ps.limit).getMany();
- return this.announcementService.packMany(announcements, me);
+ return this.announcementEntityService.packMany(announcements, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/announcements/show.ts b/packages/backend/src/server/api/endpoints/announcements/show.ts
new file mode 100644
index 0000000000..6312a0a54c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/announcements/show.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { EntityNotFoundError } from 'typeorm';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['meta'],
+
+ requireCredential: false,
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'Announcement',
+ },
+
+ errors: {
+ noSuchAnnouncement: {
+ message: 'No such announcement.',
+ code: 'NO_SUCH_ANNOUNCEMENT',
+ id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ announcementId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['announcementId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private announcementService: AnnouncementService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ try {
+ return await this.announcementService.getAnnouncement(ps.announcementId, me);
+ } catch (err) {
+ if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement);
+ throw err;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 191de8f833..ec08198514 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -64,11 +64,11 @@ export const paramDef = {
} },
caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
+ excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
- notify: { type: 'boolean' },
},
- required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
+ required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const;
@Injectable()
@@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const now = new Date();
- const antenna = await this.antennasRepository.insert({
+ const antenna = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: me.id,
@@ -124,10 +124,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
+ excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
- notify: ps.notify,
- }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 459729f61f..0c30bca9e0 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -63,11 +63,11 @@ export const paramDef = {
} },
caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
+ excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
- notify: { type: 'boolean' },
},
- required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
+ required: ['antennaId'],
} as const;
@Injectable()
@@ -83,8 +83,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ if (ps.keywords && ps.excludeKeywords) {
+ if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
+ throw new Error('either keywords or excludeKeywords is required.');
+ }
}
// Fetch the antenna
const antenna = await this.antennasRepository.findOneBy({
@@ -98,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let userList;
- if (ps.src === 'list' && ps.userListId) {
+ if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
userList = await this.userListsRepository.findOneBy({
id: ps.userListId,
userId: me.id,
@@ -112,15 +114,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.antennasRepository.update(antenna.id, {
name: ps.name,
src: ps.src,
- userListId: userList ? userList.id : null,
+ userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
+ excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
- notify: ps.notify,
isActive: true,
lastUsedAt: new Date(),
});
diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts
index 492705d6f9..ba847fc4f0 100644
--- a/packages/backend/src/server/api/endpoints/app/create.ts
+++ b/packages/backend/src/server/api/endpoints/app/create.ts
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
// Create account
- const app = await this.appsRepository.insert({
+ const app = await this.appsRepository.insertOne({
id: this.idService.gen(),
userId: me ? me.id : null,
name: ps.name,
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
permission,
callbackUrl: ps.callbackUrl,
secret: secret,
- }).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0]));
+ });
return await this.appEntityService.pack(app, null, {
detail: true,
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 26dd893138..f8ddfdb75c 100644
--- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts
+++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
@@ -78,11 +78,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const token = randomUUID();
// Create session token document
- const doc = await this.authSessionsRepository.insert({
+ const doc = await this.authSessionsRepository.insertOne({
id: this.idService.gen(),
appId: app.id,
token: token,
- }).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0]));
+ });
return {
token: doc.token,
diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts
index 2866db5424..e3a6d2d670 100644
--- a/packages/backend/src/server/api/endpoints/channels/create.ts
+++ b/packages/backend/src/server/api/endpoints/channels/create.ts
@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
- const channel = await this.channelsRepository.insert({
+ const channel = await this.channelsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
@@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive ?? false,
...(ps.color !== undefined ? { color: ps.color } : {}),
allowRenoteToExternal: ps.allowRenoteToExternal ?? true,
- } as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
+ } as MiChannel);
return await this.channelEntityService.pack(channel, me);
});
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 595a6957b2..502d42f9e0 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/find.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
folderId: ps.folderId ?? IsNull(),
});
- return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true })));
+ return await this.driveFileEntityService.packMany(files, { self: true });
});
}
}
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 c94070d9ff..08d9d9cdc3 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts
@@ -75,12 +75,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Create folder
- const folder = await this.driveFoldersRepository.insert({
+ const folder = await this.driveFoldersRepository.insertOne({
id: this.idService.gen(),
name: ps.name,
parentId: parent !== null ? parent.id : null,
userId: me.id,
- }).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0]));
+ });
const folderObj = await this.driveFolderEntityService.pack(folder);
diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts
index 2085b06365..ba48b0119e 100644
--- a/packages/backend/src/server/api/endpoints/fetch-rss.ts
+++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts
@@ -20,13 +20,188 @@ export const meta = {
res: {
type: 'object',
properties: {
+ image: {
+ type: 'object',
+ optional: true,
+ properties: {
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ url: {
+ type: 'string',
+ optional: false,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ paginationLinks: {
+ type: 'object',
+ optional: true,
+ properties: {
+ self: {
+ type: 'string',
+ optional: true,
+ },
+ first: {
+ type: 'string',
+ optional: true,
+ },
+ next: {
+ type: 'string',
+ optional: true,
+ },
+ last: {
+ type: 'string',
+ optional: true,
+ },
+ prev: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
items: {
type: 'array',
+ optional: false,
items: {
type: 'object',
+ properties: {
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ guid: {
+ type: 'string',
+ optional: true,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
+ pubDate: {
+ type: 'string',
+ optional: true,
+ },
+ creator: {
+ type: 'string',
+ optional: true,
+ },
+ summary: {
+ type: 'string',
+ optional: true,
+ },
+ content: {
+ type: 'string',
+ optional: true,
+ },
+ isoDate: {
+ type: 'string',
+ optional: true,
+ },
+ categories: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ contentSnippet: {
+ type: 'string',
+ optional: true,
+ },
+ enclosure: {
+ type: 'object',
+ optional: true,
+ properties: {
+ url: {
+ type: 'string',
+ optional: false,
+ },
+ length: {
+ type: 'number',
+ optional: true,
+ },
+ type: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ feedUrl: {
+ type: 'string',
+ optional: true,
+ },
+ description: {
+ type: 'string',
+ optional: true,
+ },
+ itunes: {
+ type: 'object',
+ optional: true,
+ additionalProperties: true,
+ properties: {
+ image: {
+ type: 'string',
+ optional: true,
+ },
+ owner: {
+ type: 'object',
+ optional: true,
+ properties: {
+ name: {
+ type: 'string',
+ optional: true,
+ },
+ email: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ author: {
+ type: 'string',
+ optional: true,
+ },
+ summary: {
+ type: 'string',
+ optional: true,
+ },
+ explicit: {
+ type: 'string',
+ optional: true,
+ },
+ categories: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ keywords: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
},
- }
- }
+ },
+ },
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts
index 584d167a29..64f13a577e 100644
--- a/packages/backend/src/server/api/endpoints/flash/create.ts
+++ b/packages/backend/src/server/api/endpoints/flash/create.ts
@@ -44,6 +44,7 @@ export const paramDef = {
permissions: { type: 'array', items: {
type: 'string',
} },
+ visibility: { type: 'string', enum: ['public', 'private'], default: 'public' },
},
required: ['title', 'summary', 'script', 'permissions'],
} as const;
@@ -58,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
- const flash = await this.flashsRepository.insert({
+ const flash = await this.flashsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
updatedAt: new Date(),
@@ -66,7 +67,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
summary: ps.summary,
script: ps.script,
permissions: ps.permissions,
- }).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
+ visibility: ps.visibility,
+ });
return await this.flashEntityService.pack(flash);
});
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 88f559138b..fa59e38976 100644
--- a/packages/backend/src/server/api/endpoints/following/requests/list.ts
+++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts
@@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
- return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));
+ return await this.followRequestEntityService.packMany(requests, 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 b07cdf1ed9..46f8998810 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error();
}
- const post = await this.galleryPostsRepository.insert(new MiGalleryPost({
+ const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({
id: this.idService.gen(),
updatedAt: new Date(),
title: ps.title,
@@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
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/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 5f738420f2..65eece5b97 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
@@ -96,10 +96,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential);
+ const keyId = keyInfo.credentialID;
- const credentialId = Buffer.from(keyInfo.credentialID).toString('base64url');
await this.userSecurityKeysRepository.insert({
- id: credentialId,
+ id: keyId,
userId: me.id,
name: ps.name,
publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'),
@@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}));
return {
- id: credentialId,
+ id: keyId,
name: ps.name,
};
});
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 8ddbe5663e..2606108539 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
me,
- (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
true,
);
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
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 390dd9cd71..d5e824df27 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
me,
- (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
true,
);
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
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 51a9cdf5a5..0f5800404e 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
me,
- (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
true,
);
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
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 a3b67301a7..bacdd5c88f 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
@@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
me,
- (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
true,
);
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index 320d9fdb00..2f619380e9 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -7,7 +7,7 @@ import { In } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
-import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js';
+import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
@@ -84,27 +84,51 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
- const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
- const notificationsRes = await this.redisClient.xrevrange(
- `notificationTimeline:${me.id}`,
- ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
- ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
- 'COUNT', limit);
+ let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
+ let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
- if (notificationsRes.length === 0) {
- return [];
- }
+ let notifications: MiNotification[];
+ for (;;) {
+ let notificationsRes: [id: string, fields: string[]][];
- let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
+ // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
+ if (sinceTime && !untilTime) {
+ notificationsRes = await this.redisClient.xrange(
+ `notificationTimeline:${me.id}`,
+ '(' + sinceTime,
+ '+',
+ 'COUNT', ps.limit);
+ } else {
+ notificationsRes = await this.redisClient.xrevrange(
+ `notificationTimeline:${me.id}`,
+ untilTime ? '(' + untilTime : '+',
+ sinceTime ? '(' + sinceTime : '-',
+ 'COUNT', ps.limit);
+ }
- if (includeTypes && includeTypes.length > 0) {
- notifications = notifications.filter(notification => includeTypes.includes(notification.type));
- } else if (excludeTypes && excludeTypes.length > 0) {
- notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
- }
+ if (notificationsRes.length === 0) {
+ return [];
+ }
- if (notifications.length === 0) {
- return [];
+ notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
+
+ if (includeTypes && includeTypes.length > 0) {
+ notifications = notifications.filter(notification => includeTypes.includes(notification.type));
+ } else if (excludeTypes && excludeTypes.length > 0) {
+ notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
+ }
+
+ if (notifications.length !== 0) {
+ // 通知が1件以上ある場合は返す
+ break;
+ }
+
+ // フィルタしたことで通知が0件になった場合、次のページを取得する
+ if (ps.sinceId && !ps.untilId) {
+ sinceTime = notificationsRes[notificationsRes.length - 1][0];
+ } else {
+ untilTime = notificationsRes[notificationsRes.length - 1][0];
+ }
}
// Mark all as read
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 3868278690..eea657ebbd 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -15,6 +15,7 @@ import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { UserAuthService } from '@/core/UserAuthService.js';
+import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -39,6 +40,12 @@ export const meta = {
code: 'UNAVAILABLE',
id: 'a2defefb-f220-8849-0af6-17f816099323',
},
+
+ emailRequired: {
+ message: 'Email address is required.',
+ code: 'EMAIL_REQUIRED',
+ id: '324c7a88-59f2-492f-903f-89134f93e47e',
+ },
},
res: {
@@ -66,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+ private metaService: MetaService,
private userEntityService: UserEntityService,
private emailService: EmailService,
private userAuthService: UserAuthService,
@@ -97,6 +105,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!res.available) {
throw new ApiError(meta.errors.unavailable);
}
+ } else if ((await this.metaService.fetch()).emailRequiredForSignup) {
+ throw new ApiError(meta.errors.emailRequired);
}
await this.userProfilesRepository.update(me.id, {
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 84a1931a3d..a8e702f328 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -498,26 +498,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private async verifyLink(url: string, user: MiLocalUser) {
if (!safeForSql(url)) return;
- const html = await this.httpRequestService.getHtml(url);
+ try {
+ const html = await this.httpRequestService.getHtml(url);
- const { window } = new JSDOM(html);
- const doc = window.document;
+ const { window } = new JSDOM(html);
+ const doc = window.document;
- const myLink = `${this.config.url}/@${user.username}`;
+ const myLink = `${this.config.url}/@${user.username}`;
- const aEls = Array.from(doc.getElementsByTagName('a'));
- const linkEls = Array.from(doc.getElementsByTagName('link'));
+ const aEls = Array.from(doc.getElementsByTagName('a'));
+ const linkEls = Array.from(doc.getElementsByTagName('link'));
- const includesMyLink = aEls.some(a => a.href === myLink);
- const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
+ const includesMyLink = aEls.some(a => a.href === myLink);
+ const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
- if (includesMyLink || includesRelMeLinks) {
- await this.userProfilesRepository.createQueryBuilder('profile').update()
- .where('userId = :userId', { userId: user.id })
- .set({
- verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
- })
- .execute();
+ if (includesMyLink || includesRelMeLinks) {
+ await this.userProfilesRepository.createQueryBuilder('profile').update()
+ .where('userId = :userId', { userId: user.id })
+ .set({
+ verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
+ })
+ .execute();
+ }
+
+ window.close();
+ } catch (err) {
+ // なにもしない
}
}
}
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 535a3ea308..c692380288 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -89,14 +89,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyWebhooks);
}
- const webhook = await this.webhooksRepository.insert({
+ const webhook = await this.webhooksRepository.insertOne({
id: this.idService.gen(),
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);
diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts
index 0ff125ad9c..a70b587da7 100644
--- a/packages/backend/src/server/api/endpoints/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/invite/create.ts
@@ -66,13 +66,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
- const ticket = await this.registrationTicketsRepository.insert({
+ const ticket = await this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
createdBy: me,
createdById: me.id,
expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null,
code: generateInviteCode(),
- }).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]));
+ });
return await this.inviteCodeEntityService.pack(ticket, me);
});
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index bfb9214439..beb77ca7ab 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -16,7 +16,7 @@ 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 { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -275,7 +275,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
- } else if (isPureRenote(renote)) {
+ } else if (isRenote(renote) && !isQuote(renote)) {
throw new ApiError(meta.errors.cannotReRenote);
}
@@ -321,7 +321,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
- } else if (isPureRenote(reply)) {
+ } else if (isRenote(reply) && !isQuote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
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 ba38573065..4fd6f8682d 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -32,6 +32,7 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
+ excludeChannels: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -86,6 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.setParameters(mutingQuery.getParameters());
//#endregion
+ //#region exclude channels
+ if (ps.excludeChannels) {
+ query.andWhere('poll.channelId IS NULL');
+ }
+ //#endregion
+
const polls = await query
.orderBy('poll.noteId', 'DESC')
.limit(ps.limit)
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 a91c506afd..f33f49075b 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
@@ -144,12 +144,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Create vote
- const vote = await this.pollVotesRepository.insert({
+ const vote = await this.pollVotesRepository.insertOne({
id: this.idService.gen(createdAt.getTime()),
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
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts
index a0a1fd9728..97b12ab7f7 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts
@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const reactions = await query.limit(ps.limit).getMany();
- return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
+ return await this.noteReactionEntityService.packMany(reactions, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index 78812351f4..38a9660aa2 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -21,7 +21,7 @@ export const meta = {
res: {
type: 'object',
- optional: false, nullable: false,
+ optional: true, nullable: false,
properties: {
sourceLang: { type: 'string' },
text: { type: 'string' },
@@ -39,6 +39,11 @@ export const meta = {
code: 'NO_SUCH_NOTE',
id: 'bea9b03f-36e0-49c5-a4db-627a029f8971',
},
+ cannotTranslateInvisibleNote: {
+ message: 'Cannot translate invisible note.',
+ code: 'CANNOT_TRANSLATE_INVISIBLE_NOTE',
+ id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d',
+ },
},
} as const;
@@ -72,17 +77,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) {
- return 204; // TODO: 良い感じのエラー返す
+ throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
}
if (note.text == null) {
- return 204;
+ return;
}
const instance = await this.metaService.fetch();
if (instance.deeplAuthKey == null) {
- return 204; // TODO: 良い感じのエラー返す
+ throw new ApiError(meta.errors.unavailable);
}
let targetLang = ps.targetLang;
diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts
index 3a02d359f8..fa03b0b457 100644
--- a/packages/backend/src/server/api/endpoints/pages/create.ts
+++ b/packages/backend/src/server/api/endpoints/pages/create.ts
@@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
});
- const page = await this.pagesRepository.insert(new MiPage({
+ const page = await this.pagesRepository.insertOne(new MiPage({
id: this.idService.gen(),
updatedAt: new Date(),
title: ps.title,
@@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
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/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts
index 85d100ce1c..48d350af59 100644
--- a/packages/backend/src/server/api/endpoints/roles/users.ts
+++ b/packages/backend/src/server/api/endpoints/roles/users.ts
@@ -92,9 +92,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
+ const _users = assigns.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
+ .then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
- user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
+ user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
})));
});
}
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 5d52ebba76..6b3389f0b2 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -6,6 +6,7 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
+import { birthdaySchema } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
@@ -66,7 +67,7 @@ export const paramDef = {
description: 'The local host is represented with `null`.',
},
- birthday: { type: 'string', nullable: true },
+ birthday: { ...birthdaySchema, nullable: true },
},
anyOf: [
{ required: ['userId'] },
@@ -127,9 +128,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.birthday) {
try {
- const d = new Date(ps.birthday);
- d.setHours(0, 0, 0, 0);
- const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
+ const birthday = ps.birthday.substring(5, 10);
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
birthdayUserQuery.select('user_profile.userId')
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
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 02aa037466..9248a2fa68 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
@@ -118,12 +118,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
// Extract top replied users
- const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit);
+ const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit);
// Make replies object (includes weights)
- const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
- user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
- weight: repliedUsers[user] / peak,
+ const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({
+ user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }),
+ weight: repliedUsers[userId] / peak,
})));
return repliesObj;
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
index e2db71c5c7..8504da0209 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
@@ -104,11 +104,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyUserLists);
}
- const userList = await this.userListsRepository.insert({
+ const userList = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
- } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
+ } as MiUserList);
const users = (await this.userListMembershipsRepository.findBy({
userListId: ps.listId,
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 952580e639..9378bde5cb 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts
@@ -65,11 +65,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyUserLists);
}
- const userList = await this.userListsRepository.insert({
+ const userList = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
- } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
+ } as MiUserList);
return await this.userListEntityService.pack(userList);
});
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index 6a5b2262fa..1d75437b81 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -132,11 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
-
- const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id)));
-
- return Array.isArray(ps.userId) ? relations : relations[0];
+ return Array.isArray(ps.userId)
+ ? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()])
+ : await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]);
});
}
}
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 1750dd6206..48e14b68cc 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -82,14 +82,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.cannotReportAdmin);
}
- const report = await this.abuseUserReportsRepository.insert({
+ const report = await this.abuseUserReportsRepository.insertOne({
id: this.idService.gen(),
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 () => {
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index bd81989cb9..062326e28d 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -110,14 +110,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// リクエストされた通りに並べ替え
+ // 順番は保持されるけど数は減ってる可能性がある
const _users: MiUser[] = [];
for (const id of ps.userIds) {
- _users.push(users.find(x => x.id === id)!);
+ const user = users.find(x => x.id === id);
+ if (user != null) _users.push(user);
}
- return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
- schema: 'UserDetailed',
- })));
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return _users.map(u => _userMap.get(u.id)!);
} else {
// Lookup user
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index 7679a9b464..2a14270a24 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -93,7 +93,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
const info = {
- operationId: endpoint.name,
+ operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない
summary: endpoint.name,
description: desc,
externalDocs: {
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index 44a143538b..a267d27fba 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -4,6 +4,10 @@
*/
import { bindThis } from '@/decorators.js';
+import { isInstanceMuted } from '@/misc/is-instance-muted.js';
+import { isUserRelated } from '@/misc/is-user-related.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { Packed } from '@/misc/json-schema.js';
import type Connection from './Connection.js';
/**
@@ -54,6 +58,24 @@ export default abstract class Channel {
return this.connection.subscriber;
}
+ /*
+ * ミュートとブロックされてるを処理する
+ */
+ protected isNoteMutedOrBlocked(note: Packed<'Note'>): boolean {
+ // 流れてきたNoteがインスタンスミュートしたインスタンスが関わる
+ if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return true;
+
+ // 流れてきたNoteがミュートしているユーザーが関わる
+ if (isUserRelated(note, this.userIdsWhoMeMuting)) return true;
+ // 流れてきたNoteがブロックされているユーザーが関わる
+ if (isUserRelated(note, this.userIdsWhoBlockingMe)) return true;
+
+ // 流れてきたNoteがリノートをミュートしてるユーザが行ったもの
+ if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true;
+
+ return false;
+ }
+
constructor(id: string, connection: Connection) {
this.id = id;
this.connection = connection;
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 135d162e63..4a1d2dd109 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -4,7 +4,6 @@
*/
import { Injectable } from '@nestjs/common';
-import { isUserRelated } from '@/misc/is-user-related.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
@@ -40,12 +39,7 @@ class AntennaChannel extends Channel {
if (data.type === 'note') {
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
this.connection.cacheNote(note);
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 90ee1ecda5..140dd3dd9b 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -4,10 +4,10 @@
*/
import { Injectable } from '@nestjs/common';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class ChannelChannel extends Channel {
@@ -38,14 +38,9 @@ class ChannelChannel extends Channel {
private async onNote(note: Packed<'Note'>) {
if (note.channelId !== this.channelId) return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
-
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
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 723b89c908..17116258d8 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -4,14 +4,12 @@
*/
import { Injectable } from '@nestjs/common';
-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 type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class GlobalTimelineChannel extends Channel {
@@ -52,26 +50,11 @@ class GlobalTimelineChannel extends Channel {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
- // 関係ない返信は除外
- if (note.reply && !this.following[note.userId]?.withReplies) {
- const reply = note.reply;
- // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
- if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
- }
-
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
-
- // Ignore notes from instances the user has muted
- if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
-
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index 377b1a0162..57bada5d9c 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -5,10 +5,10 @@
import { Injectable } from '@nestjs/common';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class HashtagChannel extends Channel {
@@ -43,14 +43,9 @@ class HashtagChannel extends Channel {
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
-
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
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 f45bf8622e..878a3180cb 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -4,12 +4,10 @@
*/
import { Injectable } from '@nestjs/common';
-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 type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class HomeTimelineChannel extends Channel {
@@ -51,9 +49,6 @@ class HomeTimelineChannel extends Channel {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
}
- // Ignore notes from instances the user has muted
- if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances))) return;
-
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
@@ -72,7 +67,7 @@ class HomeTimelineChannel extends Channel {
}
// 純粋なリノート(引用リノートでないリノート)の場合
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && note.poll == null) {
+ if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
@@ -81,14 +76,9 @@ class HomeTimelineChannel extends Channel {
}
}
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
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 d67da6f565..575d23d53c 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -4,14 +4,12 @@
*/
import { Injectable } from '@nestjs/common';
-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 type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class HybridTimelineChannel extends Channel {
@@ -71,8 +69,7 @@ class HybridTimelineChannel extends Channel {
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
}
- // Ignore notes from instances the user has muted
- if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances))) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
if (note.reply) {
const reply = note.reply;
@@ -85,14 +82,7 @@ class HybridTimelineChannel extends Channel {
}
}
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
-
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
if (this.user && note.renoteId && !note.text) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
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 43d26124ef..442d08ae51 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -4,13 +4,12 @@
*/
import { Injectable } from '@nestjs/common';
-import { checkWordMute } from '@/misc/check-word-mute.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class LocalTimelineChannel extends Channel {
@@ -61,16 +60,11 @@ class LocalTimelineChannel extends Channel {
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
-
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index 80aab4b35e..6a4ad22460 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -4,8 +4,6 @@
*/
import { Injectable } from '@nestjs/common';
-import { isUserRelated } from '@/misc/is-user-related.js';
-import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@@ -46,12 +44,7 @@ class RoleTimelineChannel extends Channel {
}
if (note.visibility !== 'public') return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
this.send('note', note);
} else {
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 f7bb106c03..14b30a157c 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -5,12 +5,11 @@
import { Inject, Injectable } from '@nestjs/common';
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
-import { isInstanceMuted } from '@/misc/is-instance-muted.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class UserListChannel extends Channel {
@@ -106,25 +105,17 @@ class UserListChannel extends Channel {
}
}
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
-
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
- // 流れてきたNoteがミュートしているインスタンスに関わるものだったら無視する
- if (isInstanceMuted(note, this.userMutedInstances)) return;
-
this.connection.cacheNote(note);
this.send('note', note);
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index b1af0c3df6..ab03489c0d 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -199,9 +199,18 @@ export class ClientServerService {
// Authenticate
fastify.addHook('onRequest', async (request, reply) => {
+ if (request.routeOptions.url == null) {
+ reply.code(404).send('Not found');
+ return;
+ }
+
// %71ueueとかでリクエストされたら困るため
const url = decodeURI(request.routeOptions.url);
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
+ if (!url.startsWith(bullBoardPath + '/static/')) {
+ reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
+ }
+
const token = request.cookies.token;
if (token == null) {
reply.code(401).send('Login required');
@@ -429,7 +438,7 @@ export class ClientServerService {
//#endregion
- const renderBase = async (reply: FastifyReply) => {
+ const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
@@ -438,6 +447,7 @@ export class ClientServerService {
title: meta.name ?? 'Misskey',
desc: meta.description,
...await this.generateCommonPugData(meta),
+ ...data,
});
};
@@ -456,7 +466,9 @@ export class ClientServerService {
};
// Atom
- fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => {
+ fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => {
+ if (request.params.user == null) return await renderBase(reply);
+
const feed = await getFeed(request.params.user);
if (feed) {
@@ -469,7 +481,9 @@ export class ClientServerService {
});
// RSS
- fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => {
+ fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => {
+ if (request.params.user == null) return await renderBase(reply);
+
const feed = await getFeed(request.params.user);
if (feed) {
@@ -482,7 +496,9 @@ export class ClientServerService {
});
// JSON
- fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => {
+ fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => {
+ if (request.params.user == null) return await renderBase(reply);
+
const feed = await getFeed(request.params.user);
if (feed) {
@@ -735,6 +751,18 @@ export class ClientServerService {
});
//#endregion
+ //region noindex pages
+ // Tags
+ fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
+ return await renderBase(reply, { noindex: true });
+ });
+
+ // User with Tags
+ fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
+ return await renderBase(reply, { noindex: true });
+ });
+ //endregion
+
fastify.get('/_info_card_', async (request, reply) => {
const meta = await this.metaService.fetch(true);
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index c6a96e94cb..8f8f08a305 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { summaly } from '@misskey-dev/summaly';
+import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
@@ -14,6 +15,7 @@ import { query } from '@/misc/prelude/url.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { ApiError } from '@/server/api/error.js';
+import { MiMeta } from '@/models/Meta.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
@@ -62,24 +64,25 @@ export class UrlPreviewService {
const meta = await this.metaService.fetch();
- this.logger.info(meta.summalyProxy
+ if (!meta.urlPreviewEnabled) {
+ reply.code(403);
+ return {
+ error: new ApiError({
+ message: 'URL preview is disabled',
+ code: 'URL_PREVIEW_DISABLED',
+ id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8',
+ }),
+ };
+ }
+
+ this.logger.info(meta.urlPreviewSummaryProxyUrl
? `(Proxy) Getting preview of ${url}@${lang} ...`
: `Getting preview of ${url}@${lang} ...`);
+
try {
- const summary = meta.summalyProxy ?
- await this.httpRequestService.getJson<ReturnType<typeof summaly>>(`${meta.summalyProxy}?${query({
- url: url,
- lang: lang ?? 'ja-JP',
- })}`)
- :
- await summaly(url, {
- followRedirects: false,
- lang: lang ?? 'ja-JP',
- agent: this.config.proxy ? {
- http: this.httpRequestService.httpAgent,
- https: this.httpRequestService.httpsAgent,
- } : undefined,
- });
+ const summary = meta.urlPreviewSummaryProxyUrl
+ ? await this.fetchSummaryFromProxy(url, meta, lang)
+ : await this.fetchSummary(url, meta, lang);
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
@@ -100,6 +103,7 @@ export class UrlPreviewService {
return summary;
} catch (err) {
this.logger.warn(`Failed to get preview of ${url}: ${err}`);
+
reply.code(422);
reply.header('Cache-Control', 'max-age=86400, immutable');
return {
@@ -111,4 +115,37 @@ export class UrlPreviewService {
};
}
}
+
+ private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
+ const agent = this.config.proxy
+ ? {
+ http: this.httpRequestService.httpAgent,
+ https: this.httpRequestService.httpsAgent,
+ }
+ : undefined;
+
+ return summaly(url, {
+ followRedirects: false,
+ lang: lang ?? 'ja-JP',
+ agent: agent,
+ userAgent: meta.urlPreviewUserAgent ?? undefined,
+ operationTimeout: meta.urlPreviewTimeout,
+ contentLengthLimit: meta.urlPreviewMaximumContentLength,
+ contentLengthRequired: meta.urlPreviewRequireContentLength,
+ });
+ }
+
+ private fetchSummaryFromProxy(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
+ const proxy = meta.urlPreviewSummaryProxyUrl!;
+ const queryStr = query({
+ url: url,
+ lang: lang ?? 'ja-JP',
+ userAgent: meta.urlPreviewUserAgent ?? undefined,
+ operationTimeout: meta.urlPreviewTimeout,
+ contentLengthLimit: meta.urlPreviewMaximumContentLength,
+ contentLengthRequired: meta.urlPreviewRequireContentLength,
+ });
+
+ return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`);
+ }
}
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 59441826b0..396536948e 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -86,8 +86,8 @@
//#endregion
//#region Script
- function importAppScript() {
- import(`/vite/${CLIENT_ENTRY}`)
+ async function importAppScript() {
+ await import(`/vite/${CLIENT_ENTRY}`)
.catch(async e => {
console.error(e);
renderError('APP_IMPORT', e);
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index 123336809b..ec1325e4e9 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -36,7 +36,7 @@ html
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842
- link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.44.0')
+ link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v3.3.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists
@@ -50,6 +50,9 @@ html
block title
= title || 'Misskey'
+ if noindex
+ meta(name='robots' content='noindex')
+
block desc
meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
index 9bc652b6a1..fb659ce171 100644
--- a/packages/backend/src/server/web/views/note.pug
+++ b/packages/backend/src/server/web/views/note.pug
@@ -2,7 +2,7 @@ extends ./base
block vars
- const user = note.user;
- - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
+ - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
- const url = `${config.url}/notes/${note.id}`;
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
- const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
@@ -28,7 +28,7 @@ block og
// FIXME: add embed player for Twitter
if images.length
meta(property='twitter:card' content='summary_large_image')
- each image in images
+ each image in images
meta(property='og:image' content= image.url)
else
meta(property='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
index 08bb08ffe7..03c50eca8a 100644
--- a/packages/backend/src/server/web/views/page.pug
+++ b/packages/backend/src/server/web/views/page.pug
@@ -3,7 +3,7 @@ extends ./base
block vars
- const user = page.user;
- const title = page.title;
- - const url = `${config.url}/@${user.username}/${page.name}`;
+ - const url = `${config.url}/@${user.username}/pages/${page.name}`;
block title
= `${title} | ${instanceName}`
diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug
index 83d57349a6..2b0a7bab5c 100644
--- a/packages/backend/src/server/web/views/user.pug
+++ b/packages/backend/src/server/web/views/user.pug
@@ -1,7 +1,7 @@
extends ./base
block vars
- - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
+ - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
- const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
block title
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 87a3c227d6..13c56b88a6 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -187,7 +187,7 @@ describe('2要素認証', () => {
}, 1000 * 60 * 2);
test('が設定でき、OTPでログインできる。', async () => {
- const registerResponse = await api('/i/2fa/register', {
+ const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
@@ -197,18 +197,18 @@ describe('2要素認証', () => {
assert.strictEqual(registerResponse.body.label, username);
assert.strictEqual(registerResponse.body.issuer, config.host);
- const doneResponse = await api('/i/2fa/done', {
+ const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const usersShowResponse = await api('/users/show', {
+ const usersShowResponse = await api('users/show', {
username,
}, alice);
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
- const signinResponse = await api('/signin', {
+ const signinResponse = await api('signin', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
@@ -216,24 +216,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
- await api('/i/2fa/unregister', {
+ await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでログインできる。', async () => {
- const registerResponse = await api('/i/2fa/register', {
+ const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
- const doneResponse = await api('/i/2fa/done', {
+ const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const registerKeyResponse = await api('/i/2fa/register-key', {
+ const registerKeyResponse = await api('i/2fa/register-key', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
@@ -243,23 +243,23 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
- const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
+ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
- }), alice);
+ }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
- const usersShowResponse = await api('/users/show', {
+ const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, true);
- const signinResponse = await api('/signin', {
+ const signinResponse = await api('signin', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
@@ -268,7 +268,7 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.allowCredentials, undefined);
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
- const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({
+ const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
keyName,
credentialId,
requestOptions: signinResponse.body,
@@ -277,24 +277,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
- await api('/i/2fa/unregister', {
+ await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
- const registerResponse = await api('/i/2fa/register', {
+ const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
- const doneResponse = await api('/i/2fa/done', {
+ const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const registerKeyResponse = await api('/i/2fa/register-key', {
+ const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@@ -302,33 +302,33 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
- const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
+ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
- }), alice);
+ }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
- const passwordLessResponse = await api('/i/2fa/password-less', {
+ const passwordLessResponse = await api('i/2fa/password-less', {
value: true,
}, alice);
assert.strictEqual(passwordLessResponse.status, 204);
- const usersShowResponse = await api('/users/show', {
+ const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
- const signinResponse = await api('/signin', {
+ const signinResponse = await api('signin', {
...signinParam(),
password: '',
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined);
- const signinResponse2 = await api('/signin', {
+ const signinResponse2 = await api('signin', {
...signinWithSecurityKeyParam({
keyName,
credentialId,
@@ -340,24 +340,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
- await api('/i/2fa/unregister', {
+ await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
- const registerResponse = await api('/i/2fa/register', {
+ const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
- const doneResponse = await api('/i/2fa/done', {
+ const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const registerKeyResponse = await api('/i/2fa/register-key', {
+ const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@@ -365,22 +365,22 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
- const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
+ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
- }), alice);
+ }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key';
- const updateKeyResponse = await api('/i/2fa/update-key', {
+ const updateKeyResponse = await api('i/2fa/update-key', {
name: renamedKey,
credentialId: credentialId.toString('base64url'),
}, alice);
assert.strictEqual(updateKeyResponse.status, 200);
- const iResponse = await api('/i', {
+ const iResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
@@ -389,24 +389,24 @@ describe('2要素認証', () => {
assert.notEqual(securityKeys[0].lastUsed, undefined);
// 後片付け
- await api('/i/2fa/unregister', {
+ await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
- const registerResponse = await api('/i/2fa/register', {
+ const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
- const doneResponse = await api('/i/2fa/done', {
+ const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const registerKeyResponse = await api('/i/2fa/register-key', {
+ const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
@@ -414,20 +414,20 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
- const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
+ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
- }), alice);
+ }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す
- const iResponse = await api('/i', {
+ const iResponse = await api('i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
for (const key of iResponse.body.securityKeysList) {
- const removeKeyResponse = await api('/i/2fa/remove-key', {
+ const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret),
password,
credentialId: key.id,
@@ -435,13 +435,13 @@ describe('2要素認証', () => {
assert.strictEqual(removeKeyResponse.status, 200);
}
- const usersShowResponse = await api('/users/show', {
+ const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, false);
- const signinResponse = await api('/signin', {
+ const signinResponse = await api('signin', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
@@ -449,43 +449,43 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
- await api('/i/2fa/unregister', {
+ await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => {
- const registerResponse = await api('/i/2fa/register', {
+ const registerResponse = await api('i/2fa/register', {
password,
}, alice);
assert.strictEqual(registerResponse.status, 200);
- const doneResponse = await api('/i/2fa/done', {
+ const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const usersShowResponse = await api('/users/show', {
+ const usersShowResponse = await api('users/show', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
- const unregisterResponse = await api('/i/2fa/unregister', {
+ const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
- const signinResponse = await api('/signin', {
+ const signinResponse = await api('signin', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
- await api('/i/2fa/unregister', {
+ await api('i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
index 1a9d5bf1f0..101238b601 100644
--- a/packages/backend/test/e2e/antennas.ts
+++ b/packages/backend/test/e2e/antennas.ts
@@ -7,7 +7,6 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
-import type { Packed } from '@/misc/json-schema.js';
import {
api,
failedApiCall,
@@ -29,10 +28,7 @@ describe('アンテナ', () => {
// エンティティとしてのアンテナを主眼においたテストを記述する
// (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからノートを取得するエンドポイントをテストする)
- // BUG misskey-jsとjson-schemaが一致していない。
- // - srcのenumにgroupが残っている
- // - userGroupIdが残っている, isActiveがない
- type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
+ type Antenna = misskey.entities.Antenna;
type User = misskey.entities.SignupResponse;
type Note = misskey.entities.Note;
@@ -42,12 +38,12 @@ describe('アンテナ', () => {
excludeKeywords: [['']],
keywords: [['keyword']],
name: 'test',
- notify: false,
src: 'all' as const,
userListId: null,
users: [''],
withFile: false,
withReplies: false,
+ excludeBots: false,
};
let root: User;
@@ -80,7 +76,7 @@ describe('アンテナ', () => {
aliceList = await userList(alice, {});
bob = await signup({ username: 'bob' });
aliceList = await userList(alice, {});
- bobFile = (await uploadFile(bob)).body;
+ bobFile = (await uploadFile(bob)).body!;
bobList = await userList(bob);
carol = await signup({ username: 'carol' });
await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
@@ -129,9 +125,9 @@ describe('アンテナ', () => {
beforeEach(async () => {
// テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) {
- const list = await api('/antennas/list', {}, user);
+ const list = await api('antennas/list', {}, user);
for (const antenna of list.body) {
- await api('/antennas/delete', { antennaId: antenna.id }, user);
+ await api('antennas/delete', { antennaId: antenna.id }, user);
}
}
});
@@ -141,11 +137,11 @@ describe('アンテナ', () => {
test('が作成できること、キーが過不足なく入っていること。', async () => {
const response = await successfulApiCall({
endpoint: 'antennas/create',
- parameters: { ...defaultParam },
+ parameters: defaultParam,
user: alice,
});
assert.match(response.id, /[0-9a-z]{10}/);
- const expected = {
+ const expected: Antenna = {
id: response.id,
caseSensitive: false,
createdAt: new Date(response.createdAt).toISOString(),
@@ -154,14 +150,15 @@ describe('アンテナ', () => {
isActive: true,
keywords: [['keyword']],
name: 'test',
- notify: false,
src: 'all',
userListId: null,
users: [''],
withFile: false,
withReplies: false,
+ excludeBots: false,
localOnly: false,
- } as Antenna;
+ notify: false,
+ };
assert.deepStrictEqual(response, expected);
});
@@ -202,27 +199,25 @@ describe('アンテナ', () => {
});
const antennaParamPattern = [
- { parameters: (): object => ({ name: 'x'.repeat(100) }) },
- { parameters: (): object => ({ name: 'x' }) },
- { parameters: (): object => ({ src: 'home' }) },
- { parameters: (): object => ({ src: 'all' }) },
- { parameters: (): object => ({ src: 'users' }) },
- { parameters: (): object => ({ src: 'list' }) },
- { parameters: (): object => ({ userListId: null }) },
- { parameters: (): object => ({ src: 'list', userListId: aliceList.id }) },
- { parameters: (): object => ({ keywords: [['x']] }) },
- { parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
- { parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
- { parameters: (): object => ({ users: [alice.username] }) },
- { parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) },
- { parameters: (): object => ({ caseSensitive: false }) },
- { parameters: (): object => ({ caseSensitive: true }) },
- { parameters: (): object => ({ withReplies: false }) },
- { parameters: (): object => ({ withReplies: true }) },
- { parameters: (): object => ({ withFile: false }) },
- { parameters: (): object => ({ withFile: true }) },
- { parameters: (): object => ({ notify: false }) },
- { parameters: (): object => ({ notify: true }) },
+ { parameters: () => ({ name: 'x'.repeat(100) }) },
+ { parameters: () => ({ name: 'x' }) },
+ { parameters: () => ({ src: 'home' as const }) },
+ { parameters: () => ({ src: 'all' as const }) },
+ { parameters: () => ({ src: 'users' as const }) },
+ { parameters: () => ({ src: 'list' as const }) },
+ { parameters: () => ({ userListId: null }) },
+ { parameters: () => ({ src: 'list' as const, userListId: aliceList.id }) },
+ { parameters: () => ({ keywords: [['x']] }) },
+ { parameters: () => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
+ { parameters: () => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
+ { parameters: () => ({ users: [alice.username] }) },
+ { parameters: () => ({ users: [alice.username, bob.username, carol.username] }) },
+ { parameters: () => ({ caseSensitive: false }) },
+ { parameters: () => ({ caseSensitive: true }) },
+ { parameters: () => ({ withReplies: false }) },
+ { parameters: () => ({ withReplies: true }) },
+ { parameters: () => ({ withFile: false }) },
+ { parameters: () => ({ withFile: true }) },
];
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
const response = await successfulApiCall({
@@ -335,7 +330,7 @@ describe('アンテナ', () => {
test.each([
{
label: '全体から',
- parameters: (): object => ({ src: 'all' }),
+ parameters: () => ({ src: 'all' }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@@ -346,7 +341,7 @@ describe('アンテナ', () => {
{
// BUG e4144a1 以降home指定は壊れている(allと同じ)
label: 'ホーム指定はallと同じ',
- parameters: (): object => ({ src: 'home' }),
+ parameters: () => ({ src: 'home' }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@@ -357,7 +352,7 @@ describe('アンテナ', () => {
{
// https://github.com/misskey-dev/misskey/issues/9025
label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
@@ -367,56 +362,56 @@ describe('アンテナ', () => {
},
{
label: 'ブロックしているユーザーのノートは含む',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
],
},
{
label: 'ブロックされているユーザーのノートは含まない',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) },
],
},
{
label: 'ミュートしているユーザーのノートは含まない',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) },
],
},
{
label: 'ミュートされているユーザーのノートは含む',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true },
],
},
{
label: '「見つけやすくする」がOFFのユーザーのノートも含まれる',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true },
],
},
{
label: '鍵付きユーザーのノートも含まれる',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true },
],
},
{
label: 'サイレンスのノートも含まれる',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true },
],
},
{
label: '削除ユーザーのノートも含まれる',
- parameters: (): object => ({}),
+ parameters: () => ({}),
posts: [
{ note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
@@ -424,7 +419,7 @@ describe('アンテナ', () => {
},
{
label: 'ユーザー指定で',
- parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
+ parameters: () => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@@ -433,7 +428,7 @@ describe('アンテナ', () => {
},
{
label: 'リスト指定で',
- parameters: (): object => ({ src: 'list', userListId: aliceList.id }),
+ parameters: () => ({ src: 'list', userListId: aliceList.id }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@@ -442,14 +437,14 @@ describe('アンテナ', () => {
},
{
label: 'CWにもマッチする',
- parameters: (): object => ({ keywords: [[keyword]] }),
+ parameters: () => ({ keywords: [[keyword]] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
],
},
{
label: 'キーワード1つ',
- parameters: (): object => ({ keywords: [[keyword]] }),
+ parameters: () => ({ keywords: [[keyword]] }),
posts: [
{ note: (): Promise<Note> => post(alice, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@@ -458,7 +453,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード3つ(AND)',
- parameters: (): object => ({ keywords: [['A', 'B', 'C']] }),
+ parameters: () => ({ keywords: [['A', 'B', 'C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test A' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A B' }) },
@@ -469,7 +464,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード3つ(OR)',
- parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }),
+ parameters: () => ({ keywords: [['A'], ['B'], ['C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true },
@@ -482,7 +477,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード3つ(AND)',
- parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }),
+ parameters: () => ({ excludeKeywords: [['A', 'B', 'C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true },
@@ -495,7 +490,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード3つ(OR)',
- parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
+ parameters: () => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) },
@@ -508,7 +503,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード1つ(大文字小文字区別する)',
- parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }),
+ parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }) },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) },
@@ -517,7 +512,7 @@ describe('アンテナ', () => {
},
{
label: 'キーワード1つ(大文字小文字区別しない)',
- parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }),
+ parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true },
@@ -526,7 +521,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード1つ(大文字小文字区別する)',
- parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
+ parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true },
@@ -536,7 +531,7 @@ describe('アンテナ', () => {
},
{
label: '除外ワード1つ(大文字小文字区別しない)',
- parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
+ parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) },
@@ -546,7 +541,7 @@ describe('アンテナ', () => {
},
{
label: '添付ファイルを問わない',
- parameters: (): object => ({ withFile: false }),
+ parameters: () => ({ withFile: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@@ -554,7 +549,7 @@ describe('アンテナ', () => {
},
{
label: '添付ファイル付きのみ',
- parameters: (): object => ({ withFile: true }),
+ parameters: () => ({ withFile: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }) },
@@ -562,7 +557,7 @@ describe('アンテナ', () => {
},
{
label: 'リプライ以外',
- parameters: (): object => ({ withReplies: false }),
+ parameters: () => ({ withReplies: false }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@@ -570,7 +565,7 @@ describe('アンテナ', () => {
},
{
label: 'リプライも含む',
- parameters: (): object => ({ withReplies: true }),
+ parameters: () => ({ withReplies: true }),
posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@@ -633,7 +628,7 @@ describe('アンテナ', () => {
endpoint: 'antennas/notes',
parameters: { antennaId: antenna.id, ...paginationParam },
user: alice,
- }) as any as Note[];
+ });
}, offsetBy, 'desc');
});
diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts
index f92384525c..c61b0c2a86 100644
--- a/packages/backend/test/e2e/api-visibility.ts
+++ b/packages/backend/test/e2e/api-visibility.ts
@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
-import { api, post, signup } from '../utils.js';
+import { UserToken, api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('API visibility', () => {
@@ -24,38 +24,38 @@ describe('API visibility', () => {
let target2: misskey.entities.SignupResponse;
/** public-post */
- let pub: any;
+ let pub: misskey.entities.Note;
/** home-post */
- let home: any;
+ let home: misskey.entities.Note;
/** followers-post */
- let fol: any;
+ let fol: misskey.entities.Note;
/** specified-post */
- let spe: any;
+ let spe: misskey.entities.Note;
/** public-reply to target's post */
- let pubR: any;
+ let pubR: misskey.entities.Note;
/** home-reply to target's post */
- let homeR: any;
+ let homeR: misskey.entities.Note;
/** followers-reply to target's post */
- let folR: any;
+ let folR: misskey.entities.Note;
/** specified-reply to target's post */
- let speR: any;
+ let speR: misskey.entities.Note;
/** public-mention to target */
- let pubM: any;
+ let pubM: misskey.entities.Note;
/** home-mention to target */
- let homeM: any;
+ let homeM: misskey.entities.Note;
/** followers-mention to target */
- let folM: any;
+ let folM: misskey.entities.Note;
/** specified-mention to target */
- let speM: any;
+ let speM: misskey.entities.Note;
/** reply target post */
- let tgt: any;
+ let tgt: misskey.entities.Note;
//#endregion
- const show = async (noteId: any, by: any) => {
- return await api('/notes/show', {
+ const show = async (noteId: misskey.entities.Note['id'], by?: UserToken) => {
+ return await api('notes/show', {
noteId,
}, by);
};
@@ -70,7 +70,7 @@ describe('API visibility', () => {
target2 = await signup({ username: 'target2' });
// follow alice <= follower
- await api('/following/create', { userId: alice.id }, follower);
+ await api('following/create', { userId: alice.id }, follower);
// normal posts
pub = await post(alice, { text: 'x', visibility: 'public' });
@@ -111,7 +111,7 @@ describe('API visibility', () => {
});
test('[show] public-postを未認証が見れる', async () => {
- const res = await show(pub.id, null);
+ const res = await show(pub.id);
assert.strictEqual(res.body.text, 'x');
});
@@ -132,7 +132,7 @@ describe('API visibility', () => {
});
test('[show] home-postを未認証が見れる', async () => {
- const res = await show(home.id, null);
+ const res = await show(home.id);
assert.strictEqual(res.body.text, 'x');
});
@@ -153,7 +153,7 @@ describe('API visibility', () => {
});
test('[show] followers-postを未認証が見れない', async () => {
- const res = await show(fol.id, null);
+ const res = await show(fol.id);
assert.strictEqual(res.body.isHidden, true);
});
@@ -179,7 +179,7 @@ describe('API visibility', () => {
});
test('[show] specified-postを未認証が見れない', async () => {
- const res = await show(spe.id, null);
+ const res = await show(spe.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
@@ -207,7 +207,7 @@ describe('API visibility', () => {
});
test('[show] public-replyを未認証が見れる', async () => {
- const res = await show(pubR.id, null);
+ const res = await show(pubR.id);
assert.strictEqual(res.body.text, 'x');
});
@@ -233,7 +233,7 @@ describe('API visibility', () => {
});
test('[show] home-replyを未認証が見れる', async () => {
- const res = await show(homeR.id, null);
+ const res = await show(homeR.id);
assert.strictEqual(res.body.text, 'x');
});
@@ -259,7 +259,7 @@ describe('API visibility', () => {
});
test('[show] followers-replyを未認証が見れない', async () => {
- const res = await show(folR.id, null);
+ const res = await show(folR.id);
assert.strictEqual(res.body.isHidden, true);
});
@@ -290,7 +290,7 @@ describe('API visibility', () => {
});
test('[show] specified-replyを未認証が見れない', async () => {
- const res = await show(speR.id, null);
+ const res = await show(speR.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
@@ -318,7 +318,7 @@ describe('API visibility', () => {
});
test('[show] public-mentionを未認証が見れる', async () => {
- const res = await show(pubM.id, null);
+ const res = await show(pubM.id);
assert.strictEqual(res.body.text, '@target x');
});
@@ -344,7 +344,7 @@ describe('API visibility', () => {
});
test('[show] home-mentionを未認証が見れる', async () => {
- const res = await show(homeM.id, null);
+ const res = await show(homeM.id);
assert.strictEqual(res.body.text, '@target x');
});
@@ -370,7 +370,7 @@ describe('API visibility', () => {
});
test('[show] followers-mentionを未認証が見れない', async () => {
- const res = await show(folM.id, null);
+ const res = await show(folM.id);
assert.strictEqual(res.body.isHidden, true);
});
@@ -401,28 +401,28 @@ describe('API visibility', () => {
});
test('[show] specified-mentionを未認証が見れない', async () => {
- const res = await show(speM.id, null);
+ const res = await show(speM.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
//#region HTL
test('[HTL] public-post が 自分が見れる', async () => {
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('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');
});
test('[HTL] public-post が 非フォロワーから見れない', async () => {
- const res = await api('/notes/timeline', { limit: 100 }, other);
+ const res = await api('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);
});
test('[HTL] followers-post が フォロワーから見れる', async () => {
- const res = await api('/notes/timeline', { limit: 100 }, follower);
+ const res = await api('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');
@@ -431,21 +431,21 @@ describe('API visibility', () => {
//#region RTL
test('[replies] followers-reply が フォロワーから見れる', async () => {
- const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
+ const res = await api('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');
});
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
- const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
+ const res = await api('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);
});
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
- const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
+ const res = await api('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');
@@ -454,14 +454,14 @@ describe('API visibility', () => {
//#region MTL
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
- const res = await api('/notes/mentions', { limit: 100 }, target);
+ const res = await api('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');
});
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
- const res = await api('/notes/mentions', { limit: 100 }, target);
+ const res = await api('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');
diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts
index b6eeec99d7..49c6a0636b 100644
--- a/packages/backend/test/e2e/api.ts
+++ b/packages/backend/test/e2e/api.ts
@@ -23,32 +23,32 @@ import type * as misskey from 'misskey-js';
describe('API', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
- let carol: misskey.entities.SignupResponse;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
- carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2);
describe('General validation', () => {
test('wrong type', async () => {
- const res = await api('/test', {
+ const res = await api('test', {
required: true,
+ // @ts-expect-error string must be string
string: 42,
});
assert.strictEqual(res.status, 400);
});
test('missing require param', async () => {
- const res = await api('/test', {
+ // @ts-expect-error required is required
+ const res = await api('test', {
string: 'a',
});
assert.strictEqual(res.status, 400);
});
test('invalid misskey:id (empty string)', async () => {
- const res = await api('/test', {
+ const res = await api('test', {
required: true,
id: '',
});
@@ -56,7 +56,7 @@ describe('API', () => {
});
test('valid misskey:id', async () => {
- const res = await api('/test', {
+ const res = await api('test', {
required: true,
id: '8wvhjghbxu',
});
@@ -64,7 +64,7 @@ describe('API', () => {
});
test('default value', async () => {
- const res = await api('/test', {
+ const res = await api('test', {
required: true,
string: 'a',
});
@@ -73,7 +73,7 @@ describe('API', () => {
});
test('can set null even if it has default value', async () => {
- const res = await api('/test', {
+ const res = await api('test', {
required: true,
nullableDefault: null,
});
@@ -82,7 +82,7 @@ describe('API', () => {
});
test('cannot set undefined if it has default value', async () => {
- const res = await api('/test', {
+ const res = await api('test', {
required: true,
nullableDefault: undefined,
});
@@ -99,14 +99,14 @@ describe('API', () => {
// aliceは管理者、APIを使える
await successfulApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: alice,
});
// bobは一般ユーザーだからダメ
await failedApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: bob,
}, {
@@ -117,7 +117,7 @@ describe('API', () => {
// publicアクセスももちろんダメ
await failedApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: undefined,
}, {
@@ -128,7 +128,7 @@ describe('API', () => {
// ごまがしもダメ
await failedApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: 'tsukawasete' },
}, {
@@ -138,13 +138,13 @@ describe('API', () => {
});
await successfulApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application2 },
});
await failedApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application },
}, {
@@ -154,7 +154,7 @@ describe('API', () => {
});
await failedApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application3 },
}, {
@@ -164,7 +164,7 @@ describe('API', () => {
});
await failedApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: { token: application4 },
}, {
@@ -177,7 +177,7 @@ describe('API', () => {
describe('Authentication header', () => {
test('一般リクエスト', async () => {
await successfulApiCall({
- endpoint: '/admin/get-index-stats',
+ endpoint: 'admin/get-index-stats',
parameters: {},
user: {
token: alice.token,
@@ -211,7 +211,7 @@ describe('API', () => {
describe('tokenエラー応答でWWW-Authenticate headerを送る', () => {
describe('invalid_token', () => {
test('一般リクエスト', async () => {
- const result = await api('/admin/get-index-stats', {}, {
+ const result = await api('admin/get-index-stats', {}, {
token: 'syuilo',
bearer: true,
});
@@ -246,7 +246,7 @@ describe('API', () => {
describe('tokenがないとrealmだけおくる', () => {
test('一般リクエスト', async () => {
- const result = await api('/admin/get-index-stats', {});
+ const result = await api('admin/get-index-stats', {});
assert.strictEqual(result.status, 401);
assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"');
});
@@ -259,7 +259,8 @@ describe('API', () => {
});
test('invalid_request', async () => {
- const result = await api('/notes/create', { text: true }, {
+ // @ts-expect-error text must be string
+ const result = await api('notes/create', { text: true }, {
token: alice.token,
bearer: true,
});
diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts
index cbd91e6e42..e4f798498f 100644
--- a/packages/backend/test/e2e/block.ts
+++ b/packages/backend/test/e2e/block.ts
@@ -22,7 +22,7 @@ describe('Block', () => {
}, 1000 * 60 * 2);
test('Block作成', async () => {
- const res = await api('/blocking/create', {
+ const res = await api('blocking/create', {
userId: bob.id,
}, alice);
@@ -30,7 +30,7 @@ describe('Block', () => {
});
test('ブロックされているユーザーをフォローできない', async () => {
- const res = await api('/following/create', { userId: alice.id }, bob);
+ const res = await api('following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
@@ -39,7 +39,7 @@ describe('Block', () => {
test('ブロックされているユーザーにリアクションできない', async () => {
const note = await post(alice, { text: 'hello' });
- const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
+ const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
@@ -48,7 +48,7 @@ describe('Block', () => {
test('ブロックされているユーザーに返信できない', async () => {
const note = await post(alice, { text: 'hello' });
- const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob);
+ const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@@ -57,7 +57,7 @@ describe('Block', () => {
test('ブロックされているユーザーのノートをRenoteできない', async () => {
const note = await post(alice, { text: 'hello' });
- const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
+ const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@@ -72,12 +72,13 @@ describe('Block', () => {
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
- const res = await api('/notes/local-timeline', {}, bob);
+ const res = await api('notes/local-timeline', {}, bob);
+ const body = res.body as misskey.entities.Note[];
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);
+ assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
+ assert.strictEqual(body.some(note => note.id === bobNote.id), true);
+ assert.strictEqual(body.some(note => note.id === carolNote.id), true);
});
});
diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts
index 2cf397e22d..ba6f9d6a65 100644
--- a/packages/backend/test/e2e/clips.ts
+++ b/packages/backend/test/e2e/clips.ts
@@ -6,47 +6,34 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
-import { JTDDataType } from 'ajv/dist/jtd';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
-import type { Packed } from '@/misc/json-schema.js';
-import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js';
-import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js';
-import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js';
-import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js';
-import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js';
-import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js';
-import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js';
-import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js';
-import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js';
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
+import type * as Misskey from 'misskey-js';
-describe('クリップ', () => {
- type User = Packed<'User'>;
- type Note = Packed<'Note'>;
- type Clip = Packed<'Clip'>;
+type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
- let alice: User;
- let bob: User;
- let aliceNote: Note;
- let aliceHomeNote: Note;
- let aliceFollowersNote: Note;
- let aliceSpecifiedNote: Note;
- let bobNote: Note;
- let bobHomeNote: Note;
- let bobFollowersNote: Note;
- let bobSpecifiedNote: Note;
+describe('クリップ', () => {
+ let alice: Misskey.entities.SignupResponse;
+ let bob: Misskey.entities.SignupResponse;
+ let aliceNote: Misskey.entities.Note;
+ let aliceHomeNote: Misskey.entities.Note;
+ let aliceFollowersNote: Misskey.entities.Note;
+ let aliceSpecifiedNote: Misskey.entities.Note;
+ let bobNote: Misskey.entities.Note;
+ let bobHomeNote: Misskey.entities.Note;
+ let bobFollowersNote: Misskey.entities.Note;
+ let bobSpecifiedNote: Misskey.entities.Note;
const compareBy = <T extends { id: string }, >(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
return selector(a).localeCompare(selector(b));
};
- type CreateParam = JTDDataType<typeof CreateParamDef>;
- const defaultCreate = (): Partial<CreateParam> => ({
+ const defaultCreate = (): Pick<Misskey.entities.ClipsCreateRequest, 'name'> => ({
name: 'test',
});
- const create = async (parameters: Partial<CreateParam> = {}, request: Partial<ApiRequest> = {}): Promise<Clip> => {
- const clip = await successfulApiCall<Clip>({
- endpoint: '/clips/create',
+ const create = async (parameters: Partial<Misskey.entities.ClipsCreateRequest> = {}, request: Partial<ApiRequest<'clips/create'>> = {}): Promise<Misskey.entities.Clip> => {
+ const clip = await successfulApiCall({
+ endpoint: 'clips/create',
parameters: {
...defaultCreate(),
...parameters,
@@ -64,17 +51,16 @@ describe('クリップ', () => {
return clip;
};
- const createMany = async (parameters: Partial<CreateParam>, count = 10, user = alice): Promise<Clip[]> => {
+ const createMany = async (parameters: Partial<Misskey.entities.ClipsCreateRequest>, count = 10, user = alice): Promise<Misskey.entities.Clip[]> => {
return await Promise.all([...Array(count)].map((_, i) => create({
name: `test${i}`,
...parameters,
}, { user })));
};
- type UpdateParam = JTDDataType<typeof UpdateParamDef>;
- const update = async (parameters: Partial<UpdateParam>, request: Partial<ApiRequest> = {}): Promise<Clip> => {
- const clip = await successfulApiCall<Clip>({
- endpoint: '/clips/update',
+ const update = async (parameters: Optional<Misskey.entities.ClipsUpdateRequest, 'name'>, request: Partial<ApiRequest<'clips/update'>> = {}): Promise<Misskey.entities.Clip> => {
+ const clip = await successfulApiCall({
+ endpoint: 'clips/update',
parameters: {
name: 'updated',
...parameters,
@@ -92,41 +78,39 @@ describe('クリップ', () => {
return clip;
};
- type DeleteParam = JTDDataType<typeof DeleteParamDef>;
- const deleteClip = async (parameters: DeleteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
- return await successfulApiCall<void>({
- endpoint: '/clips/delete',
+ const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
+ return await successfulApiCall({
+ endpoint: 'clips/delete',
parameters,
user: alice,
...request,
}, {
status: 204,
- });
+ }) as any as void;
};
- type ShowParam = JTDDataType<typeof ShowParamDef>;
- const show = async (parameters: ShowParam, request: Partial<ApiRequest> = {}): Promise<Clip> => {
- return await successfulApiCall<Clip>({
- endpoint: '/clips/show',
+ const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
+ return await successfulApiCall({
+ endpoint: 'clips/show',
parameters,
user: alice,
...request,
});
};
- const list = async (request: Partial<ApiRequest>): Promise<Clip[]> => {
- return successfulApiCall<Clip[]>({
- endpoint: '/clips/list',
+ const list = async (request: Partial<ApiRequest<'clips/list'>>): Promise<Misskey.entities.Clip[]> => {
+ return successfulApiCall({
+ endpoint: 'clips/list',
parameters: {},
user: alice,
...request,
});
};
- const usersClips = async (request: Partial<ApiRequest>): Promise<Clip[]> => {
- return await successfulApiCall<Clip[]>({
- endpoint: '/users/clips',
- parameters: {},
+ const usersClips = async (parameters: Misskey.entities.UsersClipsRequest, request: Partial<ApiRequest<'users/clips'>> = {}): Promise<Misskey.entities.Clip[]> => {
+ return await successfulApiCall({
+ endpoint: 'users/clips',
+ parameters,
user: alice,
...request,
});
@@ -136,23 +120,22 @@ describe('クリップ', () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
- // FIXME: misskey-jsのNoteはoutdatedなので直接変換できない
- aliceNote = await post(alice, { text: 'test' }) as any;
- aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any;
- aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any;
- aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any;
- bobNote = await post(bob, { text: 'test' }) as any;
- bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any;
- bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any;
- bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
+ aliceNote = await post(alice, { text: 'test' });
+ aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' });
+ aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' });
+ aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' });
+ bobNote = await post(bob, { text: 'test' });
+ bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' });
+ bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' });
+ bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' });
}, 1000 * 60 * 2);
afterEach(async () => {
// テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) {
- const list = await api('/clips/list', { limit: 11 }, user);
+ const list = await api('clips/list', { limit: 11 }, user);
for (const clip of list.body) {
- await api('/clips/delete', { clipId: clip.id }, user);
+ await api('clips/delete', { clipId: clip.id }, user);
}
}
});
@@ -177,7 +160,7 @@ describe('クリップ', () => {
}
await failedApiCall({
- endpoint: '/clips/create',
+ endpoint: 'clips/create',
parameters: defaultCreate(),
user: alice,
}, {
@@ -204,7 +187,8 @@ describe('クリップ', () => {
{ label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } },
];
test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({
- endpoint: '/clips/create',
+ endpoint: 'clips/create',
+ // @ts-expect-error invalid params
parameters: {
...defaultCreate(),
...parameters,
@@ -246,15 +230,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} },
- { label: '他人のクリップ', user: (): User => bob, assertion: {
+ { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} },
...createClipDenyPattern as any,
])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
- endpoint: '/clips/update',
+ endpoint: 'clips/update',
parameters: {
- clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
+ clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
name: 'updated',
...parameters,
},
@@ -279,14 +263,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} },
- { label: '他人のクリップ', user: (): User => bob, assertion: {
+ { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} },
])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
- endpoint: '/clips/delete',
+ endpoint: 'clips/delete',
parameters: {
- clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
+ // @ts-expect-error clipId must not be null
+ clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@@ -306,7 +291,7 @@ describe('クリップ', () => {
test('のID指定取得は他人のPrivateなクリップは取得できない', async () => {
const clip = await create({ isPublic: false }, { user: bob } );
failedApiCall({
- endpoint: '/clips/show',
+ endpoint: 'clips/show',
parameters: { clipId: clip.id },
user: alice,
}, {
@@ -323,7 +308,8 @@ describe('クリップ', () => {
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20',
} },
])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({
- endpoint: '/clips/show',
+ endpoint: 'clips/show',
+ // @ts-expect-error clipId must not be undefined
parameters: {
...parameters,
},
@@ -356,27 +342,23 @@ describe('クリップ', () => {
test('の一覧が取得できる(空)', async () => {
const res = await usersClips({
- parameters: {
- userId: alice.id,
- },
+ userId: alice.id,
});
assert.deepStrictEqual(res, []);
});
test.each([
{ label: '' },
- { label: '他人アカウントから', user: (): User => bob },
+ { label: '他人アカウントから', user: () => bob },
])('の一覧が$label取得できる', async () => {
const clips = await createMany({ isPublic: true });
const res = await usersClips({
- parameters: {
- userId: alice.id,
- },
+ userId: alice.id,
});
// 返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual(
- res.sort(compareBy<Clip>(s => s.id)),
+ res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
clips.sort(compareBy(s => s.id)));
// 認証状態で見たときだけisFavoritedが入っている
@@ -386,17 +368,16 @@ describe('クリップ', () => {
});
test.each([
- { label: '未認証', user: (): undefined => undefined },
+ { label: '未認証', user: () => undefined },
{ label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } },
])('の一覧は$labelでも取得できる', async ({ parameters, user }) => {
const clips = await createMany({ isPublic: true });
const res = await usersClips({
- parameters: {
- userId: alice.id,
- limit: clips.length,
- ...parameters,
- },
- user: (user ?? ((): User => alice))(),
+ userId: alice.id,
+ limit: clips.length,
+ ...parameters,
+ }, {
+ user: (user ?? (() => alice))(),
});
// 未認証で見たときはisFavoritedは入らない
@@ -409,10 +390,8 @@ describe('クリップ', () => {
await create({ isPublic: false });
const aliceClip = await create({ isPublic: true });
const res = await usersClips({
- parameters: {
- userId: alice.id,
- limit: 2,
- },
+ userId: alice.id,
+ limit: 2,
});
assert.deepStrictEqual(res, [aliceClip]);
});
@@ -421,17 +400,15 @@ describe('クリップ', () => {
const clips = await createMany({ isPublic: true }, 7);
clips.sort(compareBy(s => s.id));
const res = await usersClips({
- parameters: {
- userId: alice.id,
- sinceId: clips[1].id,
- untilId: clips[5].id,
- limit: 4,
- },
+ userId: alice.id,
+ sinceId: clips[1].id,
+ untilId: clips[5].id,
+ limit: 4,
});
// Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual(
- res.sort(compareBy<Clip>(s => s.id)),
+ res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
[clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない
clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id));
});
@@ -441,8 +418,9 @@ describe('クリップ', () => {
{ label: 'limitゼロ', parameters: { limit: 0 } },
{ label: 'limit最大+1', parameters: { limit: 101 } },
])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({
- endpoint: '/users/clips',
+ endpoint: 'users/clips',
parameters: {
+ // @ts-expect-error userId must not be undefined
userId: alice.id,
...parameters,
},
@@ -454,15 +432,15 @@ describe('クリップ', () => {
}));
test.each([
- { label: '作成', endpoint: '/clips/create' },
- { label: '更新', endpoint: '/clips/update' },
- { label: '削除', endpoint: '/clips/delete' },
- { label: '取得', endpoint: '/clips/list' },
- { label: 'お気に入り設定', endpoint: '/clips/favorite' },
- { label: 'お気に入り解除', endpoint: '/clips/unfavorite' },
- { label: 'お気に入り取得', endpoint: '/clips/my-favorites' },
- { label: 'ノート追加', endpoint: '/clips/add-note' },
- { label: 'ノート削除', endpoint: '/clips/remove-note' },
+ { label: '作成', endpoint: 'clips/create' as const },
+ { label: '更新', endpoint: 'clips/update' as const },
+ { label: '削除', endpoint: 'clips/delete' as const },
+ { label: '取得', endpoint: 'clips/list' as const },
+ { label: 'お気に入り設定', endpoint: 'clips/favorite' as const },
+ { label: 'お気に入り解除', endpoint: 'clips/unfavorite' as const },
+ { label: 'お気に入り取得', endpoint: 'clips/my-favorites' as const },
+ { label: 'ノート追加', endpoint: 'clips/add-note' as const },
+ { label: 'ノート削除', endpoint: 'clips/remove-note' as const },
])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({
endpoint: endpoint,
parameters: {},
@@ -474,35 +452,33 @@ describe('クリップ', () => {
}));
describe('のお気に入り', () => {
- let aliceClip: Clip;
+ let aliceClip: Misskey.entities.Clip;
- type FavoriteParam = JTDDataType<typeof FavoriteParamDef>;
- const favorite = async (parameters: FavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
- return successfulApiCall<void>({
- endpoint: '/clips/favorite',
+ const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
+ return successfulApiCall({
+ endpoint: 'clips/favorite',
parameters,
user: alice,
...request,
}, {
status: 204,
- });
+ }) as any as void;
};
- type UnfavoriteParam = JTDDataType<typeof UnfavoriteParamDef>;
- const unfavorite = async (parameters: UnfavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
- return successfulApiCall<void>({
- endpoint: '/clips/unfavorite',
+ const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
+ return successfulApiCall({
+ endpoint: 'clips/unfavorite',
parameters,
user: alice,
...request,
}, {
status: 204,
- });
+ }) as any as void;
};
- const myFavorites = async (request: Partial<ApiRequest> = {}): Promise<Clip[]> => {
- return successfulApiCall<Clip[]>({
- endpoint: '/clips/my-favorites',
+ const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
+ return successfulApiCall({
+ endpoint: 'clips/my-favorites',
parameters: {},
user: alice,
...request,
@@ -568,7 +544,7 @@ describe('クリップ', () => {
test('は同じクリップに対して二回設定できない。', async () => {
await favorite({ clipId: aliceClip.id });
await failedApiCall({
- endpoint: '/clips/favorite',
+ endpoint: 'clips/favorite',
parameters: {
clipId: aliceClip.id,
},
@@ -586,14 +562,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} },
- { label: '他人のクリップ', user: (): User => bob, assertion: {
+ { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} },
])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
- endpoint: '/clips/favorite',
+ endpoint: 'clips/favorite',
parameters: {
- clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
+ // @ts-expect-error clipId must not be null
+ clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@@ -619,7 +596,7 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '2603966e-b865-426c-94a7-af4a01241dc1',
} },
- { label: '他人のクリップ', user: (): User => bob, assertion: {
+ { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NOT_FAVORITED',
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} },
@@ -628,9 +605,10 @@ describe('クリップ', () => {
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} },
])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
- endpoint: '/clips/unfavorite',
+ endpoint: 'clips/unfavorite',
parameters: {
- clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id,
+ // @ts-expect-error clipId must not be null
+ clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters,
},
user: alice,
@@ -655,41 +633,38 @@ describe('クリップ', () => {
});
describe('に紐づくノート', () => {
- let aliceClip: Clip;
+ let aliceClip: Misskey.entities.Clip;
- const sampleNotes = (): Note[] => [
+ const sampleNotes = (): Misskey.entities.Note[] => [
aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote,
bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote,
];
- type AddNoteParam = JTDDataType<typeof AddNoteParamDef>;
- const addNote = async (parameters: AddNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
- return successfulApiCall<void>({
- endpoint: '/clips/add-note',
+ const addNote = async (parameters: Misskey.entities.ClipsAddNoteRequest, request: Partial<ApiRequest<'clips/add-note'>> = {}): Promise<void> => {
+ return successfulApiCall({
+ endpoint: 'clips/add-note',
parameters,
user: alice,
...request,
}, {
status: 204,
- });
+ }) as any as void;
};
- type RemoveNoteParam = JTDDataType<typeof RemoveNoteParamDef>;
- const removeNote = async (parameters: RemoveNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => {
- return successfulApiCall<void>({
- endpoint: '/clips/remove-note',
+ const removeNote = async (parameters: Misskey.entities.ClipsRemoveNoteRequest, request: Partial<ApiRequest<'clips/remove-note'>> = {}): Promise<void> => {
+ return successfulApiCall({
+ endpoint: 'clips/remove-note',
parameters,
user: alice,
...request,
}, {
status: 204,
- });
+ }) as any as void;
};
- type NotesParam = JTDDataType<typeof NotesParamDef>;
- const notes = async (parameters: Partial<NotesParam>, request: Partial<ApiRequest> = {}): Promise<Note[]> => {
- return successfulApiCall<Note[]>({
- endpoint: '/clips/notes',
+ const notes = async (parameters: Misskey.entities.ClipsNotesRequest, request: Partial<ApiRequest<'clips/notes'>> = {}): Promise<Misskey.entities.Note[]> => {
+ return successfulApiCall({
+ endpoint: 'clips/notes',
parameters,
user: alice,
...request,
@@ -715,7 +690,7 @@ describe('クリップ', () => {
test('として同じノートを二回紐づけることはできない', async () => {
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
await failedApiCall({
- endpoint: '/clips/add-note',
+ endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@@ -733,11 +708,11 @@ describe('クリップ', () => {
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1;
const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
text: `test ${i}`,
- }) as unknown)) as Note[];
+ }) as unknown)) as Misskey.entities.Note[];
await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id })));
await failedApiCall({
- endpoint: '/clips/add-note',
+ endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@@ -751,7 +726,7 @@ describe('クリップ', () => {
});
test('は他人のクリップへ追加できない。', async () => await failedApiCall({
- endpoint: '/clips/add-note',
+ endpoint: 'clips/add-note',
parameters: {
clipId: aliceClip.id,
noteId: aliceNote.id,
@@ -774,18 +749,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE',
id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b',
} },
- { label: '他人のクリップ', user: (): object => bob, assetion: {
+ { label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP',
id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf',
} },
])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
- endpoint: '/clips/add-note',
+ endpoint: 'clips/add-note',
parameters: {
+ // @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
+ // @ts-expect-error noteId must not be undefined
noteId: aliceNote.id,
...parameters,
},
- user: (user ?? ((): User => alice))(),
+ user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',
@@ -810,18 +787,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる
} },
- { label: '他人のクリップ', user: (): object => bob, assetion: {
+ { label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる
} },
])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
- endpoint: '/clips/remove-note',
+ endpoint: 'clips/remove-note',
parameters: {
+ // @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
+ // @ts-expect-error noteId must not be undefined
noteId: aliceNote.id,
...parameters,
},
- user: (user ?? ((): User => alice))(),
+ user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',
@@ -925,21 +904,22 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
- { label: '他人のPrivateなクリップから', user: (): object => bob, assertion: {
+ { label: '他人のPrivateなクリップから', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
- { label: '未認証でPrivateなクリップから', user: (): undefined => undefined, assertion: {
+ { label: '未認証でPrivateなクリップから', user: () => undefined, assertion: {
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} },
])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({
- endpoint: '/clips/notes',
+ endpoint: 'clips/notes',
parameters: {
+ // @ts-expect-error clipId must not be undefined
clipId: aliceClip.id,
...parameters,
},
- user: (user ?? ((): User => alice))(),
+ user: (user ?? (() => alice))(),
}, {
status: 400,
code: 'INVALID_PARAM',
diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts
index 22ec66e2af..828c5200ef 100644
--- a/packages/backend/test/e2e/drive.ts
+++ b/packages/backend/test/e2e/drive.ts
@@ -6,22 +6,14 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
-import { MiNote } from '@/models/Note.js';
-import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
+import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
-import type{ Repository } from 'typeorm'
-import type { Packed } from '@/misc/json-schema.js';
-
describe('Drive', () => {
- let Notes: Repository<MiNote>;
-
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
beforeAll(async () => {
- const connection = await initTestDb(true);
- Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
@@ -31,13 +23,13 @@ describe('Drive', () => {
const marker = Math.random().toString();
- const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'
+ const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg';
const catcher = makeStreamCatcher(
alice,
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
- (msg) => msg.body.file as Packed<'DriveFile'>,
+ (msg) => msg.body.file,
10 * 1000);
const res = await api('drive/files/upload-from-url', {
@@ -51,7 +43,7 @@ describe('Drive', () => {
assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg');
assert.strictEqual(file.type, 'image/jpeg');
- })
+ });
test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
@@ -59,27 +51,27 @@ describe('Drive', () => {
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg');
- assert.strictEqual(res.body?.type, 'image/jpeg');
- })
+ assert.strictEqual(res.body.type, 'image/jpeg');
+ });
test('添付ノート一覧を取得できる', async () => {
- const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id)
+ const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id);
const note0 = await post(alice, { fileIds: [ids[0]] });
const note1 = await post(alice, { fileIds: [ids[0], ids[1]] });
const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice);
assert.strictEqual(attached0.body.length, 2);
- assert.strictEqual(attached0.body[0].id, note1.id)
- assert.strictEqual(attached0.body[1].id, note0.id)
+ assert.strictEqual(attached0.body[0].id, note1.id);
+ assert.strictEqual(attached0.body[1].id, note0.id);
const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice);
assert.strictEqual(attached1.body.length, 1);
- assert.strictEqual(attached1.body[0].id, note1.id)
+ assert.strictEqual(attached1.body[0].id, note1.id);
const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice);
- assert.strictEqual(attached2.body.length, 0)
- })
+ assert.strictEqual(attached2.body.length, 0);
+ });
test('添付ノート一覧は他の人から見えない', async () => {
const file = await uploadFile(alice);
@@ -89,7 +81,5 @@ describe('Drive', () => {
const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual('error' in res.body, true);
-
- })
+ });
});
-
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
index d469597805..bc89dc37f4 100644
--- a/packages/backend/test/e2e/endpoints.ts
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -79,6 +79,7 @@ describe('Endpoints', () => {
test('クエリをインジェクションできない', async () => {
const res = await api('signin', {
username: 'test1',
+ // @ts-expect-error password must be string
password: {
$gt: '',
},
@@ -103,7 +104,7 @@ describe('Endpoints', () => {
const myLocation = '七森中';
const myBirthday = '2000-09-07';
- const res = await api('/i/update', {
+ const res = await api('i/update', {
name: myName,
location: myLocation,
birthday: myBirthday,
@@ -117,7 +118,7 @@ describe('Endpoints', () => {
});
test('名前を空白にできる', async () => {
- const res = await api('/i/update', {
+ const res = await api('i/update', {
name: ' ',
}, alice);
assert.strictEqual(res.status, 200);
@@ -125,11 +126,11 @@ describe('Endpoints', () => {
});
test('誕生日の設定を削除できる', async () => {
- await api('/i/update', {
+ await api('i/update', {
birthday: '2000-09-07',
}, alice);
- const res = await api('/i/update', {
+ const res = await api('i/update', {
birthday: null,
}, alice);
@@ -139,7 +140,7 @@ describe('Endpoints', () => {
});
test('不正な誕生日の形式で怒られる', async () => {
- const res = await api('/i/update', {
+ const res = await api('i/update', {
birthday: '2000/09/07',
}, alice);
assert.strictEqual(res.status, 400);
@@ -148,7 +149,7 @@ describe('Endpoints', () => {
describe('users/show', () => {
test('ユーザーが取得できる', async () => {
- const res = await api('/users/show', {
+ const res = await api('users/show', {
userId: alice.id,
}, alice);
@@ -158,14 +159,14 @@ describe('Endpoints', () => {
});
test('ユーザーが存在しなかったら怒る', async () => {
- const res = await api('/users/show', {
+ const res = await api('users/show', {
userId: '000000000000000000000000',
});
assert.strictEqual(res.status, 404);
});
test('間違ったIDで怒られる', async () => {
- const res = await api('/users/show', {
+ const res = await api('users/show', {
userId: 'kyoppie',
});
assert.strictEqual(res.status, 404);
@@ -178,7 +179,7 @@ describe('Endpoints', () => {
text: 'test',
});
- const res = await api('/notes/show', {
+ const res = await api('notes/show', {
noteId: myPost.id,
}, alice);
@@ -189,14 +190,14 @@ describe('Endpoints', () => {
});
test('投稿が存在しなかったら怒る', async () => {
- const res = await api('/notes/show', {
+ const res = await api('notes/show', {
noteId: '000000000000000000000000',
});
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
- const res = await api('/notes/show', {
+ const res = await api('notes/show', {
noteId: 'kyoppie',
});
assert.strictEqual(res.status, 400);
@@ -207,14 +208,14 @@ describe('Endpoints', () => {
test('リアクションできる', async () => {
const bobPost = await post(bob, { text: 'hi' });
- const res = await api('/notes/reactions/create', {
+ const res = await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
- const resNote = await api('/notes/show', {
+ const resNote = await api('notes/show', {
noteId: bobPost.id,
}, alice);
@@ -225,7 +226,7 @@ describe('Endpoints', () => {
test('自分の投稿にもリアクションできる', async () => {
const myPost = await post(alice, { text: 'hi' });
- const res = await api('/notes/reactions/create', {
+ const res = await api('notes/reactions/create', {
noteId: myPost.id,
reaction: '🚀',
}, alice);
@@ -236,19 +237,19 @@ describe('Endpoints', () => {
test('二重にリアクションすると上書きされる', async () => {
const bobPost = await post(bob, { text: 'hi' });
- await api('/notes/reactions/create', {
+ await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🥰',
}, alice);
- const res = await api('/notes/reactions/create', {
+ const res = await api('notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
- const resNote = await api('/notes/show', {
+ const resNote = await api('notes/show', {
noteId: bobPost.id,
}, alice);
@@ -257,7 +258,7 @@ describe('Endpoints', () => {
});
test('存在しない投稿にはリアクションできない', async () => {
- const res = await api('/notes/reactions/create', {
+ const res = await api('notes/reactions/create', {
noteId: '000000000000000000000000',
reaction: '🚀',
}, alice);
@@ -266,13 +267,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
- const res = await api('/notes/reactions/create', {}, alice);
+ // @ts-expect-error param must not be empty
+ const res = await api('notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
- const res = await api('/notes/reactions/create', {
+ const res = await api('notes/reactions/create', {
noteId: 'kyoppie',
reaction: '🚀',
}, alice);
@@ -283,7 +285,7 @@ describe('Endpoints', () => {
describe('following/create', () => {
test('フォローできる', async () => {
- const res = await api('/following/create', {
+ const res = await api('following/create', {
userId: alice.id,
}, bob);
@@ -301,7 +303,7 @@ describe('Endpoints', () => {
});
test('既にフォローしている場合は怒る', async () => {
- const res = await api('/following/create', {
+ const res = await api('following/create', {
userId: alice.id,
}, bob);
@@ -309,7 +311,7 @@ describe('Endpoints', () => {
});
test('存在しないユーザーはフォローできない', async () => {
- const res = await api('/following/create', {
+ const res = await api('following/create', {
userId: '000000000000000000000000',
}, alice);
@@ -317,7 +319,7 @@ describe('Endpoints', () => {
});
test('自分自身はフォローできない', async () => {
- const res = await api('/following/create', {
+ const res = await api('following/create', {
userId: alice.id,
}, alice);
@@ -325,13 +327,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
- const res = await api('/following/create', {}, alice);
+ // @ts-expect-error params must not be empty
+ const res = await api('following/create', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
- const res = await api('/following/create', {
+ const res = await api('following/create', {
userId: 'foo',
}, alice);
@@ -341,11 +344,11 @@ describe('Endpoints', () => {
describe('following/delete', () => {
test('フォロー解除できる', async () => {
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const res = await api('/following/delete', {
+ const res = await api('following/delete', {
userId: alice.id,
}, bob);
@@ -363,7 +366,7 @@ describe('Endpoints', () => {
});
test('フォローしていない場合は怒る', async () => {
- const res = await api('/following/delete', {
+ const res = await api('following/delete', {
userId: alice.id,
}, bob);
@@ -371,7 +374,7 @@ describe('Endpoints', () => {
});
test('存在しないユーザーはフォロー解除できない', async () => {
- const res = await api('/following/delete', {
+ const res = await api('following/delete', {
userId: '000000000000000000000000',
}, alice);
@@ -379,7 +382,7 @@ describe('Endpoints', () => {
});
test('自分自身はフォロー解除できない', async () => {
- const res = await api('/following/delete', {
+ const res = await api('following/delete', {
userId: alice.id,
}, alice);
@@ -387,13 +390,14 @@ describe('Endpoints', () => {
});
test('空のパラメータで怒られる', async () => {
- const res = await api('/following/delete', {}, alice);
+ // @ts-expect-error params must not be empty
+ const res = await api('following/delete', {}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => {
- const res = await api('/following/delete', {
+ const res = await api('following/delete', {
userId: 'kyoppie',
}, alice);
@@ -403,20 +407,20 @@ describe('Endpoints', () => {
describe('channels/search', () => {
test('空白検索で一覧を取得できる', async () => {
- await api('/channels/create', {
+ await api('channels/create', {
name: 'aaa',
description: 'bbb',
}, bob);
- await api('/channels/create', {
+ await api('channels/create', {
name: 'ccc1',
description: 'ddd1',
}, bob);
- await api('/channels/create', {
+ await api('channels/create', {
name: 'ccc2',
description: 'ddd2',
}, bob);
- const res = await api('/channels/search', {
+ const res = await api('channels/search', {
query: '',
}, bob);
@@ -425,7 +429,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 3);
});
test('名前のみの検索で名前を検索できる', async () => {
- const res = await api('/channels/search', {
+ const res = await api('channels/search', {
query: 'aaa',
type: 'nameOnly',
}, bob);
@@ -436,7 +440,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'aaa');
});
test('名前のみの検索で名前を複数検索できる', async () => {
- const res = await api('/channels/search', {
+ const res = await api('channels/search', {
query: 'ccc',
type: 'nameOnly',
}, bob);
@@ -446,7 +450,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2);
});
test('名前のみの検索で説明は検索できない', async () => {
- const res = await api('/channels/search', {
+ const res = await api('channels/search', {
query: 'bbb',
type: 'nameOnly',
}, bob);
@@ -456,7 +460,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 0);
});
test('名前と説明の検索で名前を検索できる', async () => {
- const res = await api('/channels/search', {
+ const res = await api('channels/search', {
query: 'ccc1',
}, bob);
@@ -466,7 +470,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1');
});
test('名前と説明での検索で説明を検索できる', async () => {
- const res = await api('/channels/search', {
+ const res = await api('channels/search', {
query: 'ddd1',
}, bob);
@@ -476,7 +480,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1');
});
test('名前と説明の検索で名前を複数検索できる', async () => {
- const res = await api('/channels/search', {
+ const res = await api('channels/search', {
query: 'ccc',
}, bob);
@@ -485,7 +489,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2);
});
test('名前と説明での検索で説明を複数検索できる', async () => {
- const res = await api('/channels/search', {
+ const res = await api('channels/search', {
query: 'ddd',
}, bob);
@@ -506,7 +510,7 @@ describe('Endpoints', () => {
await uploadFile(alice, {
blob: new Blob([new Uint8Array(1024)]),
});
- const res = await api('/drive', {}, alice);
+ 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).toHaveProperty('usage', 1792);
@@ -519,7 +523,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
- assert.strictEqual(res.body.name, 'Lenna.jpg');
+ assert.strictEqual(res.body!.name, 'Lenna.jpg');
});
test('ファイルに名前を付けられる', async () => {
@@ -527,7 +531,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
- assert.strictEqual(res.body.name, 'Belmond.jpg');
+ assert.strictEqual(res.body!.name, 'Belmond.jpg');
});
test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => {
@@ -535,11 +539,12 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
- assert.strictEqual(res.body.name, 'Belmond.png.jpg');
+ assert.strictEqual(res.body!.name, 'Belmond.png.jpg');
});
test('ファイル無しで怒られる', async () => {
- const res = await api('/drive/files/create', {}, alice);
+ // @ts-expect-error params must not be empty
+ const res = await api('drive/files/create', {}, alice);
assert.strictEqual(res.status, 400);
});
@@ -549,14 +554,14 @@ describe('Endpoints', () => {
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');
+ assert.strictEqual(res.body!.name, 'image.svg');
+ assert.strictEqual(res.body!.type, 'image/svg+xml');
});
for (const type of ['webp', 'avif']) {
const mediaType = `image/${type}`;
- const getWebpublicType = async (user: any, fileId: string): Promise<string> => {
+ const getWebpublicType = async (user: misskey.entities.SignupResponse, fileId: string): Promise<string> => {
// drive/files/create does not expose webpublicType directly, so get it by posting it
const res = await post(user, {
text: mediaType,
@@ -573,10 +578,10 @@ describe('Endpoints', () => {
const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200);
- assert.strictEqual(res.body.name, path);
- assert.strictEqual(res.body.type, mediaType);
+ assert.strictEqual(res.body!.name, path);
+ assert.strictEqual(res.body!.type, mediaType);
- const webpublicType = await getWebpublicType(alice, res.body.id);
+ const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp');
});
@@ -584,10 +589,10 @@ describe('Endpoints', () => {
const path = `without-alpha.${type}`;
const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200);
- assert.strictEqual(res.body.name, path);
- assert.strictEqual(res.body.type, mediaType);
+ assert.strictEqual(res.body!.name, path);
+ assert.strictEqual(res.body!.type, mediaType);
- const webpublicType = await getWebpublicType(alice, res.body.id);
+ const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp');
});
}
@@ -598,8 +603,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body;
const newName = 'いちごパスタ.png';
- const res = await api('/drive/files/update', {
- fileId: file.id,
+ const res = await api('drive/files/update', {
+ fileId: file!.id,
name: newName,
}, alice);
@@ -611,8 +616,8 @@ describe('Endpoints', () => {
test('他人のファイルは更新できない', async () => {
const file = (await uploadFile(alice)).body;
- const res = await api('/drive/files/update', {
- fileId: file.id,
+ const res = await api('drive/files/update', {
+ fileId: file!.id,
name: 'いちごパスタ.png',
}, bob);
@@ -621,12 +626,12 @@ describe('Endpoints', () => {
test('親フォルダを更新できる', async () => {
const file = (await uploadFile(alice)).body;
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const res = await api('/drive/files/update', {
- fileId: file.id,
+ const res = await api('drive/files/update', {
+ fileId: file!.id,
folderId: folder.id,
}, alice);
@@ -638,17 +643,17 @@ describe('Endpoints', () => {
test('親フォルダを無しにできる', async () => {
const file = (await uploadFile(alice)).body;
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- await api('/drive/files/update', {
- fileId: file.id,
+ await api('drive/files/update', {
+ fileId: file!.id,
folderId: folder.id,
}, alice);
- const res = await api('/drive/files/update', {
- fileId: file.id,
+ const res = await api('drive/files/update', {
+ fileId: file!.id,
folderId: null,
}, alice);
@@ -659,12 +664,12 @@ describe('Endpoints', () => {
test('他人のフォルダには入れられない', async () => {
const file = (await uploadFile(alice)).body;
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, bob)).body;
- const res = await api('/drive/files/update', {
- fileId: file.id,
+ const res = await api('drive/files/update', {
+ fileId: file!.id,
folderId: folder.id,
}, alice);
@@ -674,8 +679,8 @@ describe('Endpoints', () => {
test('存在しないフォルダで怒られる', async () => {
const file = (await uploadFile(alice)).body;
- const res = await api('/drive/files/update', {
- fileId: file.id,
+ const res = await api('drive/files/update', {
+ fileId: file!.id,
folderId: '000000000000000000000000',
}, alice);
@@ -685,8 +690,8 @@ describe('Endpoints', () => {
test('不正なフォルダIDで怒られる', async () => {
const file = (await uploadFile(alice)).body;
- const res = await api('/drive/files/update', {
- fileId: file.id,
+ const res = await api('drive/files/update', {
+ fileId: file!.id,
folderId: 'foo',
}, alice);
@@ -694,7 +699,7 @@ describe('Endpoints', () => {
});
test('ファイルが存在しなかったら怒る', async () => {
- const res = await api('/drive/files/update', {
+ const res = await api('drive/files/update', {
fileId: '000000000000000000000000',
name: 'いちごパスタ.png',
}, alice);
@@ -706,8 +711,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body;
const newName = '';
- const res = await api('/drive/files/update', {
- fileId: file.id,
+ const res = await api('drive/files/update', {
+ fileId: file!.id,
name: newName,
}, alice);
@@ -715,7 +720,7 @@ describe('Endpoints', () => {
});
test('間違ったIDで怒られる', async () => {
- const res = await api('/drive/files/update', {
+ const res = await api('drive/files/update', {
fileId: 'kyoppie',
name: 'いちごパスタ.png',
}, alice);
@@ -726,7 +731,7 @@ describe('Endpoints', () => {
describe('drive/folders/create', () => {
test('フォルダを作成できる', async () => {
- const res = await api('/drive/folders/create', {
+ const res = await api('drive/folders/create', {
name: 'test',
}, alice);
@@ -738,11 +743,11 @@ describe('Endpoints', () => {
describe('drive/folders/update', () => {
test('名前を更新できる', async () => {
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folder.id,
name: 'new name',
}, alice);
@@ -753,11 +758,11 @@ describe('Endpoints', () => {
});
test('他人のフォルダを更新できない', async () => {
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, bob)).body;
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folder.id,
name: 'new name',
}, alice);
@@ -766,14 +771,14 @@ describe('Endpoints', () => {
});
test('親フォルダを更新できる', async () => {
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const parentFolder = (await api('/drive/folders/create', {
+ const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@@ -784,18 +789,18 @@ describe('Endpoints', () => {
});
test('親フォルダを無しに更新できる', async () => {
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const parentFolder = (await api('/drive/folders/create', {
+ const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
- await api('/drive/folders/update', {
+ await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: null,
}, alice);
@@ -806,14 +811,14 @@ describe('Endpoints', () => {
});
test('他人のフォルダを親フォルダに設定できない', async () => {
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const parentFolder = (await api('/drive/folders/create', {
+ const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, bob)).body;
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@@ -822,18 +827,18 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない', async () => {
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const parentFolder = (await api('/drive/folders/create', {
+ const parentFolder = (await api('drive/folders/create', {
name: 'parent',
}, alice)).body;
- await api('/drive/folders/update', {
+ await api('drive/folders/update', {
folderId: parentFolder.id,
parentId: folder.id,
}, alice);
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id,
}, alice);
@@ -842,25 +847,25 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない(再帰的)', async () => {
- const folderA = (await api('/drive/folders/create', {
+ const folderA = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const folderB = (await api('/drive/folders/create', {
+ const folderB = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const folderC = (await api('/drive/folders/create', {
+ const folderC = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- await api('/drive/folders/update', {
+ await api('drive/folders/update', {
folderId: folderB.id,
parentId: folderA.id,
}, alice);
- await api('/drive/folders/update', {
+ await api('drive/folders/update', {
folderId: folderC.id,
parentId: folderB.id,
}, alice);
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folderA.id,
parentId: folderC.id,
}, alice);
@@ -869,11 +874,11 @@ describe('Endpoints', () => {
});
test('フォルダが循環するような構造にできない(自身)', async () => {
- const folderA = (await api('/drive/folders/create', {
+ const folderA = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folderA.id,
parentId: folderA.id,
}, alice);
@@ -882,11 +887,11 @@ describe('Endpoints', () => {
});
test('存在しない親フォルダを設定できない', async () => {
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: '000000000000000000000000',
}, alice);
@@ -895,11 +900,11 @@ describe('Endpoints', () => {
});
test('不正な親フォルダIDで怒られる', async () => {
- const folder = (await api('/drive/folders/create', {
+ const folder = (await api('drive/folders/create', {
name: 'test',
}, alice)).body;
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: folder.id,
parentId: 'foo',
}, alice);
@@ -908,7 +913,7 @@ describe('Endpoints', () => {
});
test('存在しないフォルダを更新できない', async () => {
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: '000000000000000000000000',
}, alice);
@@ -916,7 +921,7 @@ describe('Endpoints', () => {
});
test('不正なフォルダIDで怒られる', async () => {
- const res = await api('/drive/folders/update', {
+ const res = await api('drive/folders/update', {
folderId: 'foo',
}, alice);
@@ -937,7 +942,7 @@ describe('Endpoints', () => {
visibleUserIds: [alice.id],
});
- const res = await api('/notes/replies', {
+ const res = await api('notes/replies', {
noteId: alicePost.id,
}, carol);
@@ -949,7 +954,7 @@ describe('Endpoints', () => {
describe('notes/timeline', () => {
test('フォロワー限定投稿が含まれる', async () => {
- await api('/following/create', {
+ await api('following/create', {
userId: carol.id,
}, dave);
@@ -958,7 +963,7 @@ describe('Endpoints', () => {
visibility: 'followers',
});
- const res = await api('/notes/timeline', {}, dave);
+ const res = await api('notes/timeline', {}, dave);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -979,12 +984,12 @@ describe('Endpoints', () => {
test('他者に関するメモを更新できる', async () => {
const memo = '10月まで低浮上とのこと。';
- const res1 = await api('/users/update-memo', {
+ const res1 = await api('users/update-memo', {
memo,
userId: bob.id,
}, alice);
- const res2 = await api('/users/show', {
+ const res2 = await api('users/show', {
userId: bob.id,
}, alice);
assert.strictEqual(res1.status, 204);
@@ -994,12 +999,12 @@ describe('Endpoints', () => {
test('自分に関するメモを更新できる', async () => {
const memo = 'チケットを月末までに買う。';
- const res1 = await api('/users/update-memo', {
+ const res1 = await api('users/update-memo', {
memo,
userId: alice.id,
}, alice);
- const res2 = await api('/users/show', {
+ const res2 = await api('users/show', {
userId: alice.id,
}, alice);
assert.strictEqual(res1.status, 204);
@@ -1009,17 +1014,17 @@ describe('Endpoints', () => {
test('メモを削除できる', async () => {
const memo = '10月まで低浮上とのこと。';
- await api('/users/update-memo', {
+ await api('users/update-memo', {
memo,
userId: bob.id,
}, alice);
- await api('/users/update-memo', {
+ await api('users/update-memo', {
memo: '',
userId: bob.id,
}, alice);
- const res = await api('/users/show', {
+ const res = await api('users/show', {
userId: bob.id,
}, alice);
@@ -1032,21 +1037,21 @@ describe('Endpoints', () => {
const memoCarolToBob = '例の件について今度問いただす。';
await Promise.all([
- api('/users/update-memo', {
+ api('users/update-memo', {
memo: memoAliceToBob,
userId: bob.id,
}, alice),
- api('/users/update-memo', {
+ api('users/update-memo', {
memo: memoCarolToBob,
userId: bob.id,
}, carol),
]);
const [resAlice, resCarol] = await Promise.all([
- api('/users/show', {
+ api('users/show', {
userId: bob.id,
}, alice),
- api('/users/show', {
+ api('users/show', {
userId: bob.id,
}, carol),
]);
diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts
index eb03935a2a..80a5331a6d 100644
--- a/packages/backend/test/e2e/exports.ts
+++ b/packages/backend/test/e2e/exports.ts
@@ -18,7 +18,7 @@ describe('export-clips', () => {
// XXX: Any better way to get the result?
async function pollFirstDriveFile() {
while (true) {
- const files = (await api('/drive/files', {}, alice)).body;
+ const files = (await api('drive/files', {}, alice)).body;
if (!files.length) {
await new Promise(r => setTimeout(r, 100));
continue;
@@ -26,7 +26,7 @@ describe('export-clips', () => {
if (files.length > 1) {
throw new Error('Too many files?');
}
- const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body;
+ const file = (await api('drive/files/show', { fileId: files[0].id }, alice)).body;
const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
return await res.json();
}
@@ -44,16 +44,16 @@ describe('export-clips', () => {
beforeEach(async () => {
// Clean all clips and files of alice
- const clips = (await api('/clips/list', {}, alice)).body;
+ const clips = (await api('clips/list', {}, alice)).body;
for (const clip of clips) {
- const res = await api('/clips/delete', { clipId: clip.id }, alice);
+ const res = await api('clips/delete', { clipId: clip.id }, alice);
if (res.status !== 204) {
throw new Error('Failed to delete clip');
}
}
- const files = (await api('/drive/files', {}, alice)).body;
+ const files = (await api('drive/files', {}, alice)).body;
for (const file of files) {
- const res = await api('/drive/files/delete', { fileId: file.id }, alice);
+ const res = await api('drive/files/delete', { fileId: file.id }, alice);
if (res.status !== 204) {
throw new Error('Failed to delete file');
}
@@ -61,13 +61,13 @@ describe('export-clips', () => {
});
test('basic export', async () => {
- let res = await api('/clips/create', {
+ let res = await api('clips/create', {
name: 'foo',
description: 'bar',
}, alice);
assert.strictEqual(res.status, 200);
- res = await api('/i/export-clips', {}, alice);
+ res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@@ -77,7 +77,7 @@ describe('export-clips', () => {
});
test('export with notes', async () => {
- let res = await api('/clips/create', {
+ let res = await api('clips/create', {
name: 'foo',
description: 'bar',
}, alice);
@@ -96,14 +96,14 @@ describe('export-clips', () => {
});
for (const note of [note1, note2]) {
- res = await api('/clips/add-note', {
+ res = await api('clips/add-note', {
clipId: clip.id,
noteId: note.id,
}, alice);
assert.strictEqual(res.status, 204);
}
- res = await api('/i/export-clips', {}, alice);
+ res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@@ -116,14 +116,14 @@ describe('export-clips', () => {
});
test('multiple clips', async () => {
- let res = await api('/clips/create', {
+ let res = await api('clips/create', {
name: 'kawaii',
description: 'kawaii',
}, alice);
assert.strictEqual(res.status, 200);
const clip1 = res.body;
- res = await api('/clips/create', {
+ res = await api('clips/create', {
name: 'yuri',
description: 'yuri',
}, alice);
@@ -138,19 +138,19 @@ describe('export-clips', () => {
text: 'baz2',
});
- res = await api('/clips/add-note', {
+ res = await api('clips/add-note', {
clipId: clip1.id,
noteId: note1.id,
}, alice);
assert.strictEqual(res.status, 204);
- res = await api('/clips/add-note', {
+ res = await api('clips/add-note', {
clipId: clip2.id,
noteId: note2.id,
}, alice);
assert.strictEqual(res.status, 204);
- res = await api('/i/export-clips', {}, alice);
+ res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
@@ -163,7 +163,7 @@ describe('export-clips', () => {
});
test('Clipping other user\'s note', async () => {
- let res = await api('/clips/create', {
+ let res = await api('clips/create', {
name: 'kawaii',
description: 'kawaii',
}, alice);
@@ -175,13 +175,13 @@ describe('export-clips', () => {
visibility: 'followers',
});
- res = await api('/clips/add-note', {
+ res = await api('clips/add-note', {
clipId: clip.id,
noteId: note.id,
}, alice);
assert.strictEqual(res.status, 204);
- res = await api('/i/export-clips', {}, alice);
+ res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
index 74033b7dff..4851ed14be 100644
--- a/packages/backend/test/e2e/fetch-resource.ts
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -23,13 +23,13 @@ const JSON_UTF8 = 'application/json; charset=utf-8';
describe('Webリソース', () => {
let alice: misskey.entities.SignupResponse;
- let aliceUploadedFile: any;
- let alicesPost: any;
- let alicePage: any;
- let alicePlay: any;
- let aliceClip: any;
- let aliceGalleryPost: any;
- let aliceChannel: any;
+ let aliceUploadedFile: misskey.entities.DriveFile | null;
+ let alicesPost: misskey.entities.Note;
+ let alicePage: misskey.entities.Page;
+ let alicePlay: misskey.entities.Flash;
+ let aliceClip: misskey.entities.Clip;
+ let aliceGalleryPost: misskey.entities.GalleryPost;
+ let aliceChannel: misskey.entities.Channel;
let bob: misskey.entities.SignupResponse;
@@ -77,7 +77,7 @@ describe('Webリソース', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
- aliceUploadedFile = await uploadFile(alice);
+ aliceUploadedFile = (await uploadFile(alice)).body;
alicesPost = await post(alice, {
text: 'test',
});
@@ -85,7 +85,7 @@ describe('Webリソース', () => {
alicePlay = await play(alice, {});
aliceClip = await clip(alice, {});
aliceGalleryPost = await galleryPost(alice, {
- fileIds: [aliceUploadedFile.body.id],
+ fileIds: [aliceUploadedFile!.id],
});
aliceChannel = await channel(alice, {});
diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts
index b59dd8824a..5d0c70a3c2 100644
--- a/packages/backend/test/e2e/ff-visibility.ts
+++ b/packages/backend/test/e2e/ff-visibility.ts
@@ -19,15 +19,15 @@ describe('FF visibility', () => {
}, 1000 * 60 * 2);
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@@ -39,36 +39,36 @@ describe('FF visibility', () => {
test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
@@ -78,36 +78,36 @@ describe('FF visibility', () => {
test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
@@ -116,15 +116,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
@@ -136,36 +136,36 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
@@ -175,36 +175,36 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
@@ -213,15 +213,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@@ -231,34 +231,34 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
@@ -267,34 +267,34 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
@@ -302,19 +302,19 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@@ -326,45 +326,45 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
@@ -374,45 +374,45 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
@@ -421,15 +421,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
@@ -441,36 +441,36 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
@@ -480,36 +480,36 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
@@ -518,15 +518,15 @@ describe('FF visibility', () => {
});
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
@@ -536,34 +536,34 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
- const followingRes = await api('/users/following', {
+ const followingRes = await api('users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
@@ -572,34 +572,34 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
- const followersRes = await api('/users/followers', {
+ const followersRes = await api('users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
@@ -609,7 +609,7 @@ describe('FF visibility', () => {
describe('AP', () => {
test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'public',
}, alice);
@@ -617,7 +617,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 200);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'followers',
}, alice);
@@ -625,7 +625,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 403);
}
{
- await api('/i/update', {
+ await api('i/update', {
followingVisibility: 'private',
}, alice);
@@ -636,7 +636,7 @@ describe('FF visibility', () => {
test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => {
{
- await api('/i/update', {
+ await api('i/update', {
followersVisibility: 'public',
}, alice);
@@ -644,7 +644,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 200);
}
{
- await api('/i/update', {
+ await api('i/update', {
followersVisibility: 'followers',
}, alice);
@@ -652,7 +652,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 403);
}
{
- await api('/i/update', {
+ await api('i/update', {
followersVisibility: 'private',
}, alice);
diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts
index f6417e39b5..74cf61a785 100644
--- a/packages/backend/test/e2e/move.ts
+++ b/packages/backend/test/e2e/move.ts
@@ -9,7 +9,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { loadConfig } from '@/config.js';
-import { MiUser, UsersRepository } from '@/models/_.js';
+import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { jobQueue } from '@/boot/common.js';
import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js';
@@ -42,7 +42,7 @@ describe('Account Move', () => {
dave = await signup({ username: 'dave' });
eve = await signup({ username: 'eve' });
frank = await signup({ username: 'frank' });
- Users = connection.getRepository(MiUser);
+ Users = connection.getRepository(MiUser).extend(miRepository as MiRepository<MiUser>);
}, 1000 * 60 * 2);
afterAll(async () => {
@@ -55,7 +55,7 @@ describe('Account Move', () => {
}, 1000 * 10);
test('Able to create an alias', async () => {
- const res = await api('/i/update', {
+ const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
@@ -67,7 +67,7 @@ describe('Account Move', () => {
});
test('Able to create a local alias without hostname', async () => {
- await api('/i/update', {
+ await api('i/update', {
alsoKnownAs: ['@alice'],
}, bob);
@@ -77,7 +77,7 @@ describe('Account Move', () => {
});
test('Able to create a local alias without @', async () => {
- await api('/i/update', {
+ await api('i/update', {
alsoKnownAs: ['alice'],
}, bob);
@@ -87,7 +87,7 @@ describe('Account Move', () => {
});
test('Able to set remote user (but may fail)', async () => {
- const res = await api('/i/update', {
+ const res = await api('i/update', {
alsoKnownAs: ['@syuilo@example.com'],
}, bob);
@@ -97,7 +97,7 @@ describe('Account Move', () => {
});
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
- const res = await api('/i/update', {
+ const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`],
}, bob);
@@ -107,7 +107,7 @@ describe('Account Move', () => {
});
test('Unable to add itself', async () => {
- const res = await api('/i/update', {
+ const res = await api('i/update', {
alsoKnownAs: [`@bob@${url.hostname}`],
}, bob);
@@ -117,7 +117,7 @@ describe('Account Move', () => {
});
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
- const res1 = await api('/i/update', {
+ const res1 = await api('i/update', {
alsoKnownAs: [`@nonexist@${url.hostname}`],
}, bob);
@@ -125,7 +125,7 @@ describe('Account Move', () => {
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
- const res2 = await api('/i/update', {
+ const res2 = await api('i/update', {
alsoKnownAs: ['@alice', 'nonexist'],
}, bob);
@@ -135,7 +135,7 @@ describe('Account Move', () => {
});
test('Able to add two existing local account to alsoKnownAs', async () => {
- await api('/i/update', {
+ await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`],
}, bob);
@@ -146,10 +146,10 @@ describe('Account Move', () => {
});
test('Able to properly overwrite alsoKnownAs', async () => {
- await api('/i/update', {
+ await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
- await api('/i/update', {
+ await api('i/update', {
alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
}, bob);
@@ -164,79 +164,78 @@ describe('Account Move', () => {
let antennaId = '';
beforeAll(async () => {
- await api('/i/update', {
+ await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, root);
- const listRoot = await api('/users/lists/create', {
+ const listRoot = await api('users/lists/create', {
name: secureRndstr(8),
}, root);
- await api('/users/lists/push', {
+ await api('users/lists/push', {
listId: listRoot.body.id,
userId: alice.id,
}, root);
- await api('/following/create', {
+ await api('following/create', {
userId: root.id,
}, alice);
- await api('/following/create', {
+ await api('following/create', {
userId: eve.id,
}, alice);
- const antenna = await api('/antennas/create', {
+ const antenna = await api('antennas/create', {
name: secureRndstr(8),
src: 'home',
- keywords: [secureRndstr(8)],
+ keywords: [[secureRndstr(8)]],
excludeKeywords: [],
users: [],
caseSensitive: false,
localOnly: false,
withReplies: false,
withFile: false,
- notify: false,
}, alice);
antennaId = antenna.body.id;
- await api('/i/update', {
+ await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, carol);
- await api('/mute/create', {
+ await api('mute/create', {
userId: alice.id,
}, dave);
- await api('/blocking/create', {
+ await api('blocking/create', {
userId: alice.id,
}, dave);
- await api('/following/create', {
+ await api('following/create', {
userId: eve.id,
}, dave);
- await api('/following/create', {
+ await api('following/create', {
userId: dave.id,
}, eve);
- const listEve = await api('/users/lists/create', {
+ const listEve = await api('users/lists/create', {
name: secureRndstr(8),
}, eve);
- await api('/users/lists/push', {
+ await api('users/lists/push', {
listId: listEve.body.id,
userId: bob.id,
}, eve);
- await api('/i/update', {
+ await api('i/update', {
isLocked: true,
}, frank);
- await api('/following/create', {
+ await api('following/create', {
userId: frank.id,
}, alice);
- await api('/following/requests/accept', {
+ await api('following/requests/accept', {
userId: alice.id,
}, frank);
}, 1000 * 10);
test('Prohibit the root account from moving', async () => {
- const res = await api('/i/move', {
+ const res = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`,
}, root);
@@ -246,7 +245,7 @@ describe('Account Move', () => {
});
test('Unable to move to a nonexisting local account', async () => {
- const res = await api('/i/move', {
+ const res = await api('i/move', {
moveToAccount: `@nonexist@${url.hostname}`,
}, alice);
@@ -256,7 +255,7 @@ describe('Account Move', () => {
});
test('Unable to move if alsoKnownAs is invalid', async () => {
- const res = await api('/i/move', {
+ const res = await api('i/move', {
moveToAccount: `@carol@${url.hostname}`,
}, alice);
@@ -266,7 +265,7 @@ describe('Account Move', () => {
});
test('Relationships have been properly migrated', async () => {
- const move = await api('/i/move', {
+ const move = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`,
}, alice);
@@ -275,13 +274,13 @@ describe('Account Move', () => {
await sleep(1000 * 3); // wait for jobs to finish
// Unfollow delayed?
- const aliceFollowings = await api('/users/following', {
+ const aliceFollowings = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(aliceFollowings.status, 200);
assert.strictEqual(aliceFollowings.body.length, 3);
- const carolFollowings = await api('/users/following', {
+ const carolFollowings = await api('users/following', {
userId: carol.id,
}, carol);
assert.strictEqual(carolFollowings.status, 200);
@@ -289,25 +288,25 @@ describe('Account Move', () => {
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
- const blockings = await api('/blocking/list', {}, dave);
+ const blockings = await api('blocking/list', {}, dave);
assert.strictEqual(blockings.status, 200);
assert.strictEqual(blockings.body.length, 2);
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
- const mutings = await api('/mute/list', {}, dave);
+ const mutings = await api('mute/list', {}, dave);
assert.strictEqual(mutings.status, 200);
assert.strictEqual(mutings.body.length, 2);
assert.strictEqual(mutings.body[0].muteeId, bob.id);
assert.strictEqual(mutings.body[1].muteeId, alice.id);
- const rootLists = await api('/users/lists/list', {}, root);
+ const rootLists = await api('users/lists/list', {}, root);
assert.strictEqual(rootLists.status, 200);
assert.strictEqual(rootLists.body[0].userIds.length, 2);
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
- const eveLists = await api('/users/lists/list', {}, eve);
+ const eveLists = await api('users/lists/list', {}, eve);
assert.strictEqual(eveLists.status, 200);
assert.strictEqual(eveLists.body[0].userIds.length, 1);
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
@@ -315,13 +314,13 @@ describe('Account Move', () => {
test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => {
await successfulApiCall({
- endpoint: '/following/create',
+ endpoint: 'following/create',
parameters: {
userId: frank.id,
},
user: bob,
});
- const followers = await api('/users/followers', {
+ const followers = await api('users/followers', {
userId: frank.id,
}, frank);
@@ -333,7 +332,7 @@ describe('Account Move', () => {
test('Unfollowed after 10 sec (24 hours in production).', async () => {
await sleep(1000 * 8);
- const following = await api('/users/following', {
+ const following = await api('users/following', {
userId: alice.id,
}, alice);
@@ -342,7 +341,7 @@ describe('Account Move', () => {
});
test('Unable to move if the destination account has already moved.', async () => {
- const res = await api('/i/move', {
+ const res = await api('i/move', {
moveToAccount: `@alice@${url.hostname}`,
}, bob);
@@ -352,7 +351,7 @@ describe('Account Move', () => {
});
test('Follow and follower counts are properly adjusted', async () => {
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, eve);
const newAlice = await Users.findOneByOrFail({ id: alice.id });
@@ -365,7 +364,7 @@ describe('Account Move', () => {
assert.strictEqual(newEve.followingCount, 1);
assert.strictEqual(newEve.followersCount, 1);
- await api('/following/delete', {
+ await api('following/delete', {
userId: alice.id,
}, eve);
newEve = await Users.findOneByOrFail({ id: eve.id });
@@ -374,49 +373,49 @@ describe('Account Move', () => {
});
test.each([
- '/antennas/create',
- '/channels/create',
- '/channels/favorite',
- '/channels/follow',
- '/channels/unfavorite',
- '/channels/unfollow',
- '/clips/add-note',
- '/clips/create',
- '/clips/favorite',
- '/clips/remove-note',
- '/clips/unfavorite',
- '/clips/update',
- '/drive/files/upload-from-url',
- '/flash/create',
- '/flash/like',
- '/flash/unlike',
- '/flash/update',
- '/following/create',
- '/gallery/posts/create',
- '/gallery/posts/like',
- '/gallery/posts/unlike',
- '/gallery/posts/update',
- '/i/claim-achievement',
- '/i/move',
- '/i/import-blocking',
- '/i/import-following',
- '/i/import-muting',
- '/i/import-user-lists',
- '/i/pin',
- '/mute/create',
- '/notes/create',
- '/notes/favorites/create',
- '/notes/polls/vote',
- '/notes/reactions/create',
- '/pages/create',
- '/pages/like',
- '/pages/unlike',
- '/pages/update',
- '/renote-mute/create',
- '/users/lists/create',
- '/users/lists/pull',
- '/users/lists/push',
- ])('Prohibit access after moving: %s', async (endpoint) => {
+ 'antennas/create',
+ 'channels/create',
+ 'channels/favorite',
+ 'channels/follow',
+ 'channels/unfavorite',
+ 'channels/unfollow',
+ 'clips/add-note',
+ 'clips/create',
+ 'clips/favorite',
+ 'clips/remove-note',
+ 'clips/unfavorite',
+ 'clips/update',
+ 'drive/files/upload-from-url',
+ 'flash/create',
+ 'flash/like',
+ 'flash/unlike',
+ 'flash/update',
+ 'following/create',
+ 'gallery/posts/create',
+ 'gallery/posts/like',
+ 'gallery/posts/unlike',
+ 'gallery/posts/update',
+ 'i/claim-achievement',
+ 'i/move',
+ 'i/import-blocking',
+ 'i/import-following',
+ 'i/import-muting',
+ 'i/import-user-lists',
+ 'i/pin',
+ 'mute/create',
+ 'notes/create',
+ 'notes/favorites/create',
+ 'notes/polls/vote',
+ 'notes/reactions/create',
+ 'pages/create',
+ 'pages/like',
+ 'pages/unlike',
+ 'pages/update',
+ 'renote-mute/create',
+ 'users/lists/create',
+ 'users/lists/pull',
+ 'users/lists/push',
+ ] as const)('Prohibit access after moving: %s', async (endpoint) => {
const res = await api(endpoint, {}, alice);
assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
@@ -424,18 +423,17 @@ describe('Account Move', () => {
});
test('Prohibit access after moving: /antennas/update', async () => {
- const res = await api('/antennas/update', {
+ const res = await api('antennas/update', {
antennaId,
name: secureRndstr(8),
src: 'users',
- keywords: [secureRndstr(8)],
+ keywords: [[secureRndstr(8)]],
excludeKeywords: [],
users: [eve.id],
caseSensitive: false,
localOnly: false,
withReplies: false,
withFile: false,
- notify: false,
}, alice);
assert.strictEqual(res.status, 403);
@@ -447,12 +445,12 @@ describe('Account Move', () => {
const res = await uploadFile(alice);
assert.strictEqual(res.status, 403);
- assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
- assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
+ assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
+ assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
});
test('Prohibit updating alsoKnownAs after moving', async () => {
- const res = await api('/i/update', {
+ const res = await api('i/update', {
alsoKnownAs: [`@eve@${url.hostname}`],
}, alice);
diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts
index 1d28e07b7d..0e52c5decc 100644
--- a/packages/backend/test/e2e/mute.ts
+++ b/packages/backend/test/e2e/mute.ts
@@ -19,21 +19,31 @@ describe('Mute', () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
+
+ // Mute: alice ==> carol
+ await api('mute/create', {
+ userId: carol.id,
+ }, alice);
}, 1000 * 60 * 2);
test('ミュート作成', async () => {
- const res = await api('/mute/create', {
- userId: carol.id,
+ const res = await api('mute/create', {
+ userId: bob.id,
}, alice);
assert.strictEqual(res.status, 204);
+
+ // 単体でも走らせられるように副作用消す
+ await api('mute/delete', {
+ userId: bob.id,
+ }, alice);
});
test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' });
- const res = await api('/notes/mentions', {}, alice);
+ const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -43,11 +53,11 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット
- await api('/i/read-all-unread-notes', {}, alice);
+ await api('i/read-all-unread-notes', {}, alice);
await post(carol, { text: '@alice hi' });
- const res = await api('/i', {}, alice);
+ const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
@@ -55,7 +65,7 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット
- await api('/i/read-all-unread-notes', {}, alice);
+ await api('i/read-all-unread-notes', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
@@ -64,8 +74,8 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット
- await api('/i/read-all-unread-notes', {}, alice);
- await api('/notifications/mark-all-as-read', {}, alice);
+ await api('i/read-all-unread-notes', {}, alice);
+ await api('notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
@@ -78,7 +88,7 @@ describe('Mute', () => {
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
- const res = await api('/notes/local-timeline', {}, alice);
+ const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -94,7 +104,7 @@ describe('Mute', () => {
renoteId: carolNote.id,
});
- const res = await api('/notes/local-timeline', {}, alice);
+ const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -110,7 +120,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
- const res = await api('/i/notifications', {}, alice);
+ const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -123,7 +133,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
- const res = await api('/i/notifications', {}, alice);
+ const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -137,7 +147,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' });
- const res = await api('/i/notifications', {}, alice);
+ const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -151,7 +161,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id });
- const res = await api('/i/notifications', {}, alice);
+ const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -165,7 +175,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id });
- const res = await api('/i/notifications', {}, alice);
+ const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -175,30 +185,36 @@ describe('Mute', () => {
});
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
- await api('/i/follow', { userId: alice.id }, bob);
- await api('/i/follow', { userId: alice.id }, carol);
+ await api('following/create', { userId: alice.id }, bob);
+ await api('following/create', { userId: alice.id }, carol);
- const res = await api('/i/notifications', {}, alice);
+ const res = await api('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);
+
+ await api('following/delete', { userId: alice.id }, bob);
+ await api('following/delete', { userId: alice.id }, carol);
});
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
- await api('/i/update/', { isLocked: true }, alice);
- await api('/following/create', { userId: alice.id }, bob);
- await api('/following/create', { userId: alice.id }, carol);
+ await api('i/update', { isLocked: true }, alice);
+ await api('following/create', { userId: alice.id }, bob);
+ await api('following/create', { userId: alice.id }, carol);
- const res = await api('/i/notifications', {}, alice);
+ const res = await api('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);
+
+ await api('following/delete', { userId: alice.id }, bob);
+ await api('following/delete', { userId: alice.id }, carol);
});
});
@@ -208,7 +224,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
- const res = await api('/i/notifications-grouped', {}, alice);
+ const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -220,7 +236,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
- const res = await api('/i/notifications-grouped', {}, alice);
+ const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -234,7 +250,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' });
- const res = await api('/i/notifications-grouped', {}, alice);
+ const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -248,7 +264,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id });
- const res = await api('/i/notifications-grouped', {}, alice);
+ const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -262,7 +278,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id });
- const res = await api('/i/notifications-grouped', {}, alice);
+ const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -272,24 +288,27 @@ describe('Mute', () => {
});
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
- await api('/i/follow', { userId: alice.id }, bob);
- await api('/i/follow', { userId: alice.id }, carol);
+ await api('following/create', { userId: alice.id }, bob);
+ await api('following/create', { userId: alice.id }, carol);
- const res = await api('/i/notifications-grouped', {}, alice);
+ const res = await api('i/notifications-grouped', {}, 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);
+
+ await api('following/delete', { userId: alice.id }, bob);
+ await api('following/delete', { userId: alice.id }, carol);
});
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
- await api('/i/update/', { isLocked: true }, alice);
- await api('/following/create', { userId: alice.id }, bob);
- await api('/following/create', { userId: alice.id }, carol);
+ await api('i/update', { isLocked: true }, alice);
+ await api('following/create', { userId: alice.id }, bob);
+ await api('following/create', { userId: alice.id }, carol);
- const res = await api('/i/notifications-grouped', {}, alice);
+ const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts
index 2406204f41..bda31d9640 100644
--- a/packages/backend/test/e2e/note.ts
+++ b/packages/backend/test/e2e/note.ts
@@ -8,12 +8,13 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { MiNote } from '@/models/Note.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
-import { api, initTestDb, post, signup, uploadFile, uploadUrl } from '../utils.js';
+import { api, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('Note', () => {
let Notes: any;
+ let root: misskey.entities.SignupResponse;
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let tom: misskey.entities.SignupResponse;
@@ -21,6 +22,7 @@ describe('Note', () => {
beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
+ root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
tom = await signup({ username: 'tom', host: 'example.com' });
@@ -31,7 +33,7 @@ describe('Note', () => {
text: 'test',
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@@ -41,7 +43,7 @@ describe('Note', () => {
test('ファイルを添付できる', async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
fileIds: [file.id],
}, alice);
@@ -53,7 +55,7 @@ describe('Note', () => {
test('他人のファイルで怒られる', async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
text: 'test',
fileIds: [file.id],
}, alice);
@@ -64,7 +66,7 @@ describe('Note', () => {
}, 1000 * 10);
test('存在しないファイルで怒られる', async () => {
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
text: 'test',
fileIds: ['000000000000000000000000'],
}, alice);
@@ -75,7 +77,7 @@ describe('Note', () => {
});
test('不正なファイルIDで怒られる', async () => {
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
fileIds: ['kyoppie'],
}, alice);
assert.strictEqual(res.status, 400);
@@ -93,7 +95,7 @@ describe('Note', () => {
replyId: bobPost.id,
};
- const res = await api('/notes/create', alicePost, alice);
+ const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@@ -111,7 +113,7 @@ describe('Note', () => {
renoteId: bobPost.id,
};
- const res = await api('/notes/create', alicePost, alice);
+ const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@@ -129,7 +131,7 @@ describe('Note', () => {
renoteId: bobPost.id,
};
- const res = await api('/notes/create', alicePost, alice);
+ const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@@ -142,7 +144,7 @@ describe('Note', () => {
const bobPost = await post(bob, {
text: 'test',
});
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
text: ' ',
renoteId: bobPost.id,
}, alice);
@@ -152,7 +154,7 @@ describe('Note', () => {
});
test('visibility: followersでrenoteできる', async () => {
- const createRes = await api('/notes/create', {
+ const createRes = await api('notes/create', {
text: 'test',
visibility: 'followers',
}, alice);
@@ -160,7 +162,7 @@ describe('Note', () => {
assert.strictEqual(createRes.status, 200);
const renoteId = createRes.body.createdNote.id;
- const renoteRes = await api('/notes/create', {
+ const renoteRes = await api('notes/create', {
visibility: 'followers',
renoteId,
}, alice);
@@ -169,7 +171,7 @@ describe('Note', () => {
assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId);
assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers');
- const deleteRes = await api('/notes/delete', {
+ const deleteRes = await api('notes/delete', {
noteId: renoteRes.body.createdNote.id,
}, alice);
@@ -177,11 +179,11 @@ describe('Note', () => {
});
test('visibility: followersなノートに対してフォロワーはリプライできる', async () => {
- await api('/following/create', {
+ await api('following/create', {
userId: alice.id,
}, bob);
- const aliceNote = await api('/notes/create', {
+ const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'followers',
}, alice);
@@ -189,7 +191,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
const replyId = aliceNote.body.createdNote.id;
- const bobReply = await api('/notes/create', {
+ const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId,
}, bob);
@@ -197,20 +199,20 @@ describe('Note', () => {
assert.strictEqual(bobReply.status, 200);
assert.strictEqual(bobReply.body.createdNote.replyId, replyId);
- await api('/following/delete', {
+ await api('following/delete', {
userId: alice.id,
}, bob);
});
test('visibility: followersなノートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => {
- const aliceNote = await api('/notes/create', {
+ const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'followers',
}, alice);
assert.strictEqual(aliceNote.status, 200);
- const bobReply = await api('/notes/create', {
+ const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id,
}, bob);
@@ -220,7 +222,7 @@ describe('Note', () => {
});
test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => {
- const aliceNote = await api('/notes/create', {
+ const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'specified',
visibleUserIds: [bob.id],
@@ -228,7 +230,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
- const bobReply = await api('/notes/create', {
+ const bobReply = await api('notes/create', {
text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id,
visibility: 'specified',
@@ -239,7 +241,7 @@ describe('Note', () => {
});
test('visibility: specifiedなノートに対してvisibility: follwersで返信しようとすると怒られる', async () => {
- const aliceNote = await api('/notes/create', {
+ const aliceNote = await api('notes/create', {
text: 'direct note to bob',
visibility: 'specified',
visibleUserIds: [bob.id],
@@ -247,7 +249,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200);
- const bobReply = await api('/notes/create', {
+ const bobReply = await api('notes/create', {
text: 'reply to alice note with visibility: followers',
replyId: aliceNote.body.createdNote.id,
visibility: 'followers',
@@ -261,7 +263,7 @@ describe('Note', () => {
const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
});
@@ -269,7 +271,7 @@ describe('Note', () => {
const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@@ -278,7 +280,7 @@ describe('Note', () => {
text: 'test',
replyId: '000000000000000000000000',
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@@ -286,7 +288,7 @@ describe('Note', () => {
const post = {
renoteId: '000000000000000000000000',
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@@ -295,7 +297,7 @@ describe('Note', () => {
text: 'test',
replyId: 'foo',
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@@ -303,7 +305,7 @@ describe('Note', () => {
const post = {
renoteId: 'foo',
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400);
});
@@ -312,7 +314,7 @@ describe('Note', () => {
text: '@ghost yo',
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@@ -324,7 +326,7 @@ describe('Note', () => {
text: '@bob @bob @bob yo',
};
- const res = await api('/notes/create', post, alice);
+ const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@@ -337,25 +339,25 @@ describe('Note', () => {
describe('添付ファイル情報', () => {
test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
- const res = await api('/notes/create', {
- fileIds: [file.body.id],
+ const res = await api('notes/create', {
+ fileIds: [file.body!.id],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.files.length, 1);
- assert.strictEqual(res.body.createdNote.files[0].id, file.body.id);
+ assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
});
test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
- const createdNote = await api('/notes/create', {
- fileIds: [file.body.id],
+ const createdNote = await api('notes/create', {
+ fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
- const res = await api('/notes', {
+ const res = await api('notes', {
withFiles: true,
}, alice);
@@ -364,23 +366,23 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.files.length, 1);
- assert.strictEqual(myNote.files[0].id, file.body.id);
+ assert.strictEqual(myNote.files[0].id, file.body!.id);
});
test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
- const createdNote = await api('/notes/create', {
- fileIds: [file.body.id],
+ const createdNote = await api('notes/create', {
+ fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
- const renoted = await api('/notes/create', {
+ const renoted = await api('notes/create', {
renoteId: createdNote.body.createdNote.id,
}, alice);
assert.strictEqual(renoted.status, 200);
- const res = await api('/notes', {
+ const res = await api('notes', {
renote: true,
}, alice);
@@ -389,24 +391,24 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.renote.files.length, 1);
- assert.strictEqual(myNote.renote.files[0].id, file.body.id);
+ assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
});
test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
- const createdNote = await api('/notes/create', {
- fileIds: [file.body.id],
+ const createdNote = await api('notes/create', {
+ fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
- const reply = await api('/notes/create', {
+ const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id,
text: 'this is reply',
}, alice);
assert.strictEqual(reply.status, 200);
- const res = await api('/notes', {
+ const res = await api('notes', {
reply: true,
}, alice);
@@ -415,29 +417,29 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.reply.files.length, 1);
- assert.strictEqual(myNote.reply.files[0].id, file.body.id);
+ assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
});
test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice);
- const createdNote = await api('/notes/create', {
- fileIds: [file.body.id],
+ const createdNote = await api('notes/create', {
+ fileIds: [file.body!.id],
}, alice);
assert.strictEqual(createdNote.status, 200);
- const reply = await api('/notes/create', {
+ const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id,
text: 'this is reply',
}, alice);
assert.strictEqual(reply.status, 200);
- const renoted = await api('/notes/create', {
+ const renoted = await api('notes/create', {
renoteId: reply.body.createdNote.id,
}, alice);
assert.strictEqual(renoted.status, 200);
- const res = await api('/notes', {
+ const res = await api('notes', {
renote: true,
}, alice);
@@ -446,7 +448,7 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null);
assert.strictEqual(myNote.renote.reply.files.length, 1);
- assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id);
+ assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
});
test('NSFWが強制されている場合変更できない', async () => {
@@ -472,26 +474,26 @@ describe('Note', () => {
priority: 0,
value: true,
},
- },
- }, alice);
+ } as any,
+ }, root);
assert.strictEqual(res.status, 200);
const assign = await api('admin/roles/assign', {
userId: alice.id,
roleId: res.body.id,
- }, alice);
+ }, root);
assert.strictEqual(assign.status, 204);
- assert.strictEqual(file.body.isSensitive, false);
+ assert.strictEqual(file.body!.isSensitive, false);
const nsfwfile = await uploadFile(alice);
assert.strictEqual(nsfwfile.status, 200);
- assert.strictEqual(nsfwfile.body.isSensitive, true);
+ assert.strictEqual(nsfwfile.body!.isSensitive, true);
const liftnsfw = await api('drive/files/update', {
- fileId: nsfwfile.body.id,
+ fileId: nsfwfile.body!.id,
isSensitive: false,
}, alice);
@@ -499,7 +501,7 @@ describe('Note', () => {
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
const oldaddnsfw = await api('drive/files/update', {
- fileId: file.body.id,
+ fileId: file.body!.id,
isSensitive: true,
}, alice);
@@ -508,17 +510,17 @@ describe('Note', () => {
await api('admin/roles/unassign', {
userId: alice.id,
roleId: res.body.id,
- });
+ }, root);
await api('admin/roles/delete', {
roleId: res.body.id,
- }, alice);
+ }, root);
});
});
describe('notes/create', () => {
test('投票を添付できる', async () => {
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
text: 'test',
poll: {
choices: ['foo', 'bar'],
@@ -531,14 +533,15 @@ describe('Note', () => {
});
test('投票の選択肢が無くて怒られる', async () => {
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
+ // @ts-expect-error poll must not be empty
poll: {},
}, alice);
assert.strictEqual(res.status, 400);
});
test('投票の選択肢が無くて怒られる (空の配列)', async () => {
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
poll: {
choices: [],
},
@@ -547,7 +550,7 @@ describe('Note', () => {
});
test('投票の選択肢が1つで怒られる', async () => {
- const res = await api('/notes/create', {
+ const res = await api('notes/create', {
poll: {
choices: ['Strawberry Pasta'],
},
@@ -556,14 +559,14 @@ describe('Note', () => {
});
test('投票できる', async () => {
- const { body } = await api('/notes/create', {
+ const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
}, alice);
- const res = await api('/notes/polls/vote', {
+ const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
@@ -572,19 +575,19 @@ describe('Note', () => {
});
test('複数投票できない', async () => {
- const { body } = await api('/notes/create', {
+ const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
}, alice);
- await api('/notes/polls/vote', {
+ await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0,
}, alice);
- const res = await api('/notes/polls/vote', {
+ const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2,
}, alice);
@@ -593,7 +596,7 @@ describe('Note', () => {
});
test('許可されている場合は複数投票できる', async () => {
- const { body } = await api('/notes/create', {
+ const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
@@ -601,17 +604,17 @@ describe('Note', () => {
},
}, alice);
- await api('/notes/polls/vote', {
+ await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0,
}, alice);
- await api('/notes/polls/vote', {
+ await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
- const res = await api('/notes/polls/vote', {
+ const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2,
}, alice);
@@ -620,7 +623,7 @@ describe('Note', () => {
});
test('締め切られている場合は投票できない', async () => {
- const { body } = await api('/notes/create', {
+ const { body } = await api('notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
@@ -630,7 +633,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2));
- const res = await api('/notes/polls/vote', {
+ const res = await api('notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
@@ -643,13 +646,13 @@ describe('Note', () => {
sensitiveWords: [
'test',
],
- }, alice);
+ }, root);
assert.strictEqual(sensitive.status, 204);
await new Promise(x => setTimeout(x, 2));
- const note1 = await api('/notes/create', {
+ const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@@ -662,11 +665,11 @@ describe('Note', () => {
sensitiveWords: [
'/Test/i',
],
- }, alice);
+ }, root);
assert.strictEqual(sensitive.status, 204);
- const note2 = await api('/notes/create', {
+ const note2 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@@ -679,11 +682,11 @@ describe('Note', () => {
sensitiveWords: [
'Test hoge',
],
- }, alice);
+ }, root);
assert.strictEqual(sensitive.status, 204);
- const note2 = await api('/notes/create', {
+ const note2 = await api('notes/create', {
text: 'hogeTesthuge',
}, alice);
@@ -696,13 +699,13 @@ describe('Note', () => {
prohibitedWords: [
'test',
],
- }, alice);
+ }, root);
assert.strictEqual(prohibited.status, 204);
await new Promise(x => setTimeout(x, 2));
- const note1 = await api('/notes/create', {
+ const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@@ -715,11 +718,11 @@ describe('Note', () => {
prohibitedWords: [
'/Test/i',
],
- }, alice);
+ }, root);
assert.strictEqual(prohibited.status, 204);
- const note2 = await api('/notes/create', {
+ const note2 = await api('notes/create', {
text: 'hogetesthuge',
}, alice);
@@ -732,11 +735,11 @@ describe('Note', () => {
prohibitedWords: [
'Test hoge',
],
- }, alice);
+ }, root);
assert.strictEqual(prohibited.status, 204);
- const note2 = await api('/notes/create', {
+ const note2 = await api('notes/create', {
text: 'hogeTesthuge',
}, alice);
@@ -749,13 +752,13 @@ describe('Note', () => {
prohibitedWords: [
'test',
],
- }, alice);
+ }, root);
assert.strictEqual(prohibited.status, 204);
await new Promise(x => setTimeout(x, 2));
- const note1 = await api('/notes/create', {
+ const note1 = await api('notes/create', {
text: 'hogetesthuge',
}, tom);
@@ -783,8 +786,8 @@ describe('Note', () => {
priority: 1,
value: 0,
},
- },
- }, alice);
+ } as any,
+ }, root);
assert.strictEqual(res.status, 200);
@@ -793,13 +796,13 @@ describe('Note', () => {
const assign = await api('admin/roles/assign', {
userId: alice.id,
roleId: res.body.id,
- }, alice);
+ }, root);
assert.strictEqual(assign.status, 204);
await new Promise(x => setTimeout(x, 2));
- const note = await api('/notes/create', {
+ const note = await api('notes/create', {
text: '@bob potentially annoying text',
}, alice);
@@ -809,11 +812,11 @@ describe('Note', () => {
await api('admin/roles/unassign', {
userId: alice.id,
roleId: res.body.id,
- });
+ }, root);
await api('admin/roles/delete', {
roleId: res.body.id,
- }, alice);
+ }, root);
});
test('ダイレクト投稿もエラーになる', async () => {
@@ -837,8 +840,8 @@ describe('Note', () => {
priority: 1,
value: 0,
},
- },
- }, alice);
+ } as any,
+ }, root);
assert.strictEqual(res.status, 200);
@@ -847,16 +850,16 @@ describe('Note', () => {
const assign = await api('admin/roles/assign', {
userId: alice.id,
roleId: res.body.id,
- }, alice);
+ }, root);
assert.strictEqual(assign.status, 204);
await new Promise(x => setTimeout(x, 2));
- const note = await api('/notes/create', {
+ const note = await api('notes/create', {
text: 'potentially annoying text',
visibility: 'specified',
- visibleUserIds: [ bob.id ],
+ visibleUserIds: [bob.id],
}, alice);
assert.strictEqual(note.status, 400);
@@ -865,11 +868,11 @@ describe('Note', () => {
await api('admin/roles/unassign', {
userId: alice.id,
roleId: res.body.id,
- });
+ }, root);
await api('admin/roles/delete', {
roleId: res.body.id,
- }, alice);
+ }, root);
});
test('ダイレクトの宛先とメンションが同じ場合は重複してカウントしない', async () => {
@@ -893,8 +896,8 @@ describe('Note', () => {
priority: 1,
value: 1,
},
- },
- }, alice);
+ } as any,
+ }, root);
assert.strictEqual(res.status, 200);
@@ -903,16 +906,16 @@ describe('Note', () => {
const assign = await api('admin/roles/assign', {
userId: alice.id,
roleId: res.body.id,
- }, alice);
+ }, root);
assert.strictEqual(assign.status, 204);
await new Promise(x => setTimeout(x, 2));
- const note = await api('/notes/create', {
+ const note = await api('notes/create', {
text: '@bob potentially annoying text',
visibility: 'specified',
- visibleUserIds: [ bob.id ],
+ visibleUserIds: [bob.id],
}, alice);
assert.strictEqual(note.status, 200);
@@ -920,11 +923,11 @@ describe('Note', () => {
await api('admin/roles/unassign', {
userId: alice.id,
roleId: res.body.id,
- });
+ }, root);
await api('admin/roles/delete', {
roleId: res.body.id,
- }, alice);
+ }, root);
});
});
@@ -959,4 +962,61 @@ describe('Note', () => {
assert.strictEqual(mainNote.repliesCount, 0);
});
});
+
+ describe('notes/translate', () => {
+ describe('翻訳機能の利用が許可されていない場合', () => {
+ let cannotTranslateRole: misskey.entities.Role;
+
+ beforeAll(async () => {
+ cannotTranslateRole = await role(root, {}, { canUseTranslator: false });
+ await api('admin/roles/assign', { roleId: cannotTranslateRole.id, userId: alice.id }, root);
+ });
+
+ test('翻訳機能の利用が許可されていない場合翻訳できない', async () => {
+ const aliceNote = await post(alice, { text: 'Hello' });
+ const res = await api('notes/translate', {
+ noteId: aliceNote.id,
+ targetLang: 'ja',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
+ });
+
+ afterAll(async () => {
+ await api('admin/roles/unassign', { roleId: cannotTranslateRole.id, userId: alice.id }, root);
+ });
+ });
+
+ test('存在しないノートは翻訳できない', async () => {
+ const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
+
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.code, 'NO_SUCH_NOTE');
+ });
+
+ test('不可視なノートは翻訳できない', async () => {
+ const aliceNote = await post(alice, { visibility: 'followers', text: 'Hello' });
+ const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
+
+ assert.strictEqual(bobTranslateAttempt.status, 400);
+ assert.strictEqual(bobTranslateAttempt.body.error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
+ });
+
+ test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => {
+ const aliceNote = await post(alice, { text: null, poll: { choices: ['kinoko', 'takenoko'] } });
+ const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice);
+
+ assert.strictEqual(res.status, 204);
+ });
+
+ test('サーバーに DeepL 認証キーが登録されていない場合翻訳できない', async () => {
+ const aliceNote = await post(alice, { text: 'Hello' });
+ const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice);
+
+ // NOTE: デフォルトでは登録されていないので落ちる
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
+ });
+ });
});
diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts
index 403de0cb8d..1abbb4f044 100644
--- a/packages/backend/test/e2e/renote-mute.ts
+++ b/packages/backend/test/e2e/renote-mute.ts
@@ -22,7 +22,7 @@ describe('Renote Mute', () => {
}, 1000 * 60 * 2);
test('ミュート作成', async () => {
- const res = await api('/renote-mute/create', {
+ const res = await api('renote-mute/create', {
userId: carol.id,
}, alice);
@@ -37,7 +37,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ
await sleep(100);
- const res = await api('/notes/local-timeline', {}, alice);
+ const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -54,7 +54,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ
await sleep(100);
- const res = await api('/notes/local-timeline', {}, alice);
+ const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -63,6 +63,22 @@ describe('Renote Mute', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
});
+ // #12956
+ test('タイムラインにリノートミュートしているユーザーの通常ノートのリノートが含まれる', async () => {
+ const carolNote = await post(carol, { text: 'hi' });
+ const bobRenote = await post(bob, { renoteId: carolNote.id });
+
+ // redisに追加されるのを待つ
+ await sleep(100);
+
+ const res = await api('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 === carolNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+ });
+
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
const bobNote = await post(bob, { text: 'hi' });
@@ -86,4 +102,17 @@ describe('Renote Mute', () => {
assert.strictEqual(fired, true);
});
+
+ // #12956
+ test('ストリームにリノートミュートしているユーザーの通常ノートのリノートが流れてくる', async () => {
+ const carolbNote = await post(carol, { text: 'hi' });
+
+ const fired = await waitFire(
+ alice, 'localTimeline',
+ () => api('notes/create', { renoteId: carolbNote.id }, bob),
+ msg => msg.type === 'note' && msg.body.userId === bob.id,
+ );
+
+ assert.strictEqual(fired, true);
+ });
});
diff --git a/packages/backend/test/e2e/reversi-game.ts b/packages/backend/test/e2e/reversi-game.ts
new file mode 100644
index 0000000000..788255beac
--- /dev/null
+++ b/packages/backend/test/e2e/reversi-game.ts
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { ReversiMatchResponse } from 'misskey-js/entities.js';
+import { api, signup } from '../utils.js';
+import type * as misskey from 'misskey-js';
+
+describe('ReversiGame', () => {
+ let alice: misskey.entities.SignupResponse;
+ let bob: misskey.entities.SignupResponse;
+
+ beforeAll(async () => {
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ }, 1000 * 60 * 2);
+
+ test('matches when alice invites bob and bob accepts', async () => {
+ const response1 = await api('reversi/match', { userId: bob.id }, alice);
+ assert.strictEqual(response1.status, 204);
+ assert.strictEqual(response1.body, null);
+ const response2 = await api('reversi/match', { userId: alice.id }, bob);
+ assert.strictEqual(response2.status, 200);
+ assert.notStrictEqual(response2.body, null);
+ const body = response2.body as ReversiMatchResponse;
+ assert.strictEqual(body.user1.id, alice.id);
+ assert.strictEqual(body.user2.id, bob.id);
+ });
+});
diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts
index 57ce73ba60..b0a70074c6 100644
--- a/packages/backend/test/e2e/streaming.ts
+++ b/packages/backend/test/e2e/streaming.ts
@@ -63,7 +63,7 @@ describe('Streaming', () => {
takumiNote = await post(takumi, { text: 'piyo' });
// Follow: ayano => kyoko
- await api('following/create', { userId: kyoko.id }, ayano);
+ await api('following/create', { userId: kyoko.id, withReplies: false }, ayano);
// Follow: ayano => akari
await follow(ayano, akari);
@@ -158,19 +158,17 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
- /* なんか失敗する
test('フォローしているユーザーの visibility: followers な投稿への返信が流れる', async () => {
- const note = await api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko);
+ const note = await post(kyoko, { text: 'foo', visibility: 'followers' });
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
- () => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.body.id }, kyoko), // kyoko posts
+ () => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.id }, kyoko), // kyoko posts
msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo',
);
assert.strictEqual(fired, true);
});
- */
test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => {
const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' });
@@ -511,6 +509,16 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
+
+ test('withReplies = falseでフォローしてる人によるリプライが流れてくる', async () => {
+ const fired = await waitFire(
+ ayano, 'globalTimeline', // ayano:Global
+ () => api('notes/create', { text: 'foo', replyId: kanakoNote.id }, kyoko), // kyoko posts
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, true);
+ });
});
describe('UserList Timeline', () => {
@@ -601,7 +609,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのノートがリストTLに流れない', async () => {
- await api('/i/update', {
+ await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);
@@ -618,7 +626,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのノートに対するリプライがリストTLに流れない', async () => {
- await api('/i/update', {
+ await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);
@@ -635,7 +643,7 @@ describe('Streaming', () => {
// #10443
test('ミュートしているサーバのノートに対するリノートがリストTLに流れない', async () => {
- await api('/i/update', {
+ await api('i/update', {
mutedInstances: ['example.com'],
}, chitose);
diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts
index b4570cdef1..53bb6eb765 100644
--- a/packages/backend/test/e2e/thread-mute.ts
+++ b/packages/backend/test/e2e/thread-mute.ts
@@ -24,12 +24,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
- await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+ await api('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 api('/notes/mentions', {}, alice);
+ const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
@@ -40,15 +40,15 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット
- await api('/i/read-all-unread-notes', {}, alice);
+ await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
- await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+ await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
- const res = await api('/i', {}, alice);
+ const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
@@ -56,11 +56,11 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise<void>(async done => {
// 状態リセット
- await api('/i/read-all-unread-notes', {}, alice);
+ await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
- await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+ await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
let fired = false;
@@ -84,12 +84,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
- await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+ await api('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 api('/i/notifications', {}, alice);
+ const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index 0e71d707dd..5487292afc 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -26,7 +26,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
@@ -35,14 +35,14 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーのノートが含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -51,14 +51,14 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
const carolNote = await post(carol, { text: 'hi' });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
@@ -68,14 +68,14 @@ describe('Timelines', () => {
test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -84,15 +84,15 @@ describe('Timelines', () => {
test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/following/update', { userId: bob.id, withReplies: true }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -101,15 +101,15 @@ describe('Timelines', () => {
test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/following/update', { userId: bob.id, withReplies: true }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -118,15 +118,15 @@ describe('Timelines', () => {
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/following/update', { userId: bob.id, withReplies: true }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -135,17 +135,17 @@ describe('Timelines', () => {
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/following/create', { userId: carol.id }, alice);
- await api('/following/create', { userId: carol.id }, bob);
- await api('/following/update', { userId: bob.id, withReplies: true }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: carol.id }, alice);
+ await api('following/create', { userId: carol.id }, bob);
+ await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
@@ -155,16 +155,16 @@ describe('Timelines', () => {
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/following/create', { userId: carol.id }, alice);
- await api('/following/update', { userId: bob.id, withReplies: true }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: carol.id }, alice);
+ await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
@@ -173,14 +173,14 @@ describe('Timelines', () => {
test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -189,14 +189,14 @@ describe('Timelines', () => {
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
@@ -210,7 +210,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
@@ -219,14 +219,14 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { renoteId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -235,14 +235,14 @@ describe('Timelines', () => {
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { renoteId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', {
+ const res = await api('notes/timeline', {
withRenotes: false,
}, alice);
@@ -253,14 +253,14 @@ describe('Timelines', () => {
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', {
+ const res = await api('notes/timeline', {
withRenotes: false,
}, alice);
@@ -271,13 +271,13 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -285,15 +285,15 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/mute/create', { userId: carol.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -302,16 +302,16 @@ describe('Timelines', () => {
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/following/update', { userId: bob.id, withReplies: true }, alice);
- await api('/mute/create', { userId: carol.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('following/update', { userId: bob.id, withReplies: true }, alice);
+ await api('mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -321,13 +321,13 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
const bobNote = await post(bob, { text: 'hi' });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -336,13 +336,13 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -350,7 +350,7 @@ describe('Timelines', () => {
test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const [bobFile, carolFile] = await Promise.all([
uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
@@ -363,7 +363,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100, withFiles: true }, alice);
+ const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -374,14 +374,14 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
- await api('/following/create', { userId: bob.id }, alice);
+ const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -393,7 +393,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
@@ -402,13 +402,13 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
@@ -421,7 +421,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -429,13 +429,13 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -448,7 +448,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok');
@@ -463,7 +463,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'ok');
@@ -479,7 +479,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/timeline', { limit: 100 }, alice);
+ const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -494,7 +494,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -508,7 +508,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
@@ -522,7 +522,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -531,12 +531,12 @@ describe('Timelines', () => {
test.concurrent('チャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
+ const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -548,7 +548,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -557,14 +557,14 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーの visibility: home なノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: carol.id }, alice);
+ await api('following/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
const bobNote = await post(bob, { text: 'hi' });
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -573,14 +573,14 @@ describe('Timelines', () => {
test.concurrent('ミュートしているユーザーのノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/mute/create', { userId: carol.id }, alice);
+ await api('mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi' });
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -589,15 +589,15 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/mute/create', { userId: carol.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -606,16 +606,16 @@ describe('Timelines', () => {
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- await api('/following/update', { userId: bob.id, withReplies: true }, alice);
- await api('/mute/create', { userId: carol.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ await api('following/update', { userId: bob.id, withReplies: true }, alice);
+ await api('mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
@@ -624,14 +624,14 @@ describe('Timelines', () => {
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
@@ -645,7 +645,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100, withReplies: true }, alice);
+ const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -659,7 +659,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100, withFiles: true }, alice);
+ const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -674,7 +674,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -686,7 +686,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -694,13 +694,13 @@ describe('Timelines', () => {
test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -708,14 +708,14 @@ describe('Timelines', () => {
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
@@ -729,7 +729,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
@@ -742,7 +742,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/local-timeline', { limit: 100 }, alice);
+ const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -751,13 +751,13 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
const bobNote = await post(bob, { text: 'hi' });
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -766,13 +766,13 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -785,7 +785,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100, withReplies: true }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -799,7 +799,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/notes/hybrid-timeline', { limit: 100, withFiles: true }, alice);
+ const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -810,14 +810,14 @@ describe('Timelines', () => {
test.concurrent('リスインしているフォローしていないユーザーのノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi' });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -825,14 +825,14 @@ describe('Timelines', () => {
test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -840,14 +840,14 @@ describe('Timelines', () => {
test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -855,15 +855,15 @@ describe('Timelines', () => {
test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -871,15 +871,15 @@ describe('Timelines', () => {
test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -888,32 +888,50 @@ describe('Timelines', () => {
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
await sleep(1000);
const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id, withReplies: false }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
+ test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
+ const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
+ await sleep(1000);
+ const carolNote = await post(carol, { text: 'hi' });
+ const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
+
+ await waitForPushToTl();
+
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
+
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+ });
+
test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
- await api('/users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -921,15 +939,15 @@ describe('Timelines', () => {
test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -937,15 +955,15 @@ describe('Timelines', () => {
test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
@@ -954,14 +972,14 @@ describe('Timelines', () => {
test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => {
const [alice] = await Promise.all([signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: alice.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: alice.id }, alice);
await sleep(1000);
const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
@@ -970,15 +988,15 @@ describe('Timelines', () => {
test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -986,15 +1004,15 @@ describe('Timelines', () => {
test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -1003,14 +1021,14 @@ describe('Timelines', () => {
test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
@@ -1019,15 +1037,15 @@ describe('Timelines', () => {
test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
- await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
- await api('/users/lists/push', { listId: list.id, userId: carol.id }, alice);
+ const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
+ await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
+ await api('users/lists/push', { listId: list.id, userId: carol.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl();
- const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
+ const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -1041,7 +1059,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id }, alice);
+ const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -1053,7 +1071,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id }, alice);
+ const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -1061,13 +1079,13 @@ describe('Timelines', () => {
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/following/create', { userId: bob.id }, alice);
+ await api('following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id }, alice);
+ const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
@@ -1080,7 +1098,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: alice.id }, alice);
+ const res = await api('users/notes', { userId: alice.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
@@ -1089,12 +1107,12 @@ describe('Timelines', () => {
test.concurrent('チャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
+ const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id }, alice);
+ const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -1108,7 +1126,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id }, alice);
+ const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
@@ -1123,7 +1141,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice);
+ const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -1138,7 +1156,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice);
+ const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
@@ -1153,7 +1171,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id, withFiles: true }, alice);
+ const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -1162,12 +1180,12 @@ describe('Timelines', () => {
test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
+ const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice);
+ const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -1175,12 +1193,12 @@ describe('Timelines', () => {
test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- const channel = await api('/channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body);
+ const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice);
+ const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -1188,12 +1206,12 @@ describe('Timelines', () => {
test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => {
const [bob] = await Promise.all([signup()]);
- const channel = await api('/channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body);
+ const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, bob);
+ const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
@@ -1201,14 +1219,14 @@ describe('Timelines', () => {
test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
- await api('/mute/create', { userId: carol.id }, alice);
+ await api('mute/create', { userId: carol.id }, alice);
await sleep(1000);
const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id }, alice);
+ const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
@@ -1216,7 +1234,7 @@ describe('Timelines', () => {
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
- await api('/mute/create', { userId: bob.id }, alice);
+ await api('mute/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
@@ -1224,7 +1242,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id }, alice);
+ const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
@@ -1238,7 +1256,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: alice.id, withReplies: true }, alice);
+ const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
});
@@ -1250,7 +1268,7 @@ describe('Timelines', () => {
await waitForPushToTl();
- const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice);
+ const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts
index 6897cf08c6..331e053935 100644
--- a/packages/backend/test/e2e/user-notes.ts
+++ b/packages/backend/test/e2e/user-notes.ts
@@ -11,9 +11,9 @@ import type * as misskey from 'misskey-js';
describe('users/notes', () => {
let alice: misskey.entities.SignupResponse;
- let jpgNote: any;
- let pngNote: any;
- let jpgPngNote: any;
+ let jpgNote: misskey.entities.Note;
+ let pngNote: misskey.entities.Note;
+ let jpgPngNote: misskey.entities.Note;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
@@ -31,7 +31,7 @@ describe('users/notes', () => {
}, 1000 * 60 * 2);
test('withFiles', async () => {
- const res = await api('/users/notes', {
+ const res = await api('users/notes', {
userId: alice.id,
withFiles: true,
}, alice);
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 3cf2a5dee1..3458e06384 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -8,7 +8,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { inspect } from 'node:util';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
-import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
+import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('ユーザー', () => {
@@ -24,31 +24,12 @@ describe('ユーザー', () => {
}, {});
};
- // BUG misskey-jsとjson-schemaと実際に返ってくるデータが全部違う
- type UserLite = misskey.entities.UserLite & {
- badgeRoles: any[],
- };
-
- type UserDetailedNotMe = UserLite &
- misskey.entities.UserDetailed & {
- roles: any[],
- };
-
- type MeDetailed = UserDetailedNotMe &
- misskey.entities.MeDetailed & {
- achievements: object[],
- loggedInDays: number,
- policies: object,
- };
-
- type User = MeDetailed & { token: string };
-
- const show = async (id: string, me = root): Promise<MeDetailed | UserDetailedNotMe> => {
- return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any;
+ const show = async (id: string, me = root): Promise<misskey.entities.UserDetailed> => {
+ return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me });
};
// UserLiteのキーが過不足なく入っている?
- const userLite = (user: User): Partial<UserLite> => {
+ const userLite = (user: misskey.entities.UserLite): Partial<misskey.entities.UserLite> => {
return stripUndefined({
id: user.id,
name: user.name,
@@ -71,7 +52,7 @@ describe('ユーザー', () => {
};
// UserDetailedNotMeのキーが過不足なく入っている?
- const userDetailedNotMe = (user: User): Partial<UserDetailedNotMe> => {
+ const userDetailedNotMe = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({
...userLite(user),
url: user.url,
@@ -111,7 +92,7 @@ describe('ユーザー', () => {
};
// Relations関連のキーが過不足なく入っている?
- const userDetailedNotMeWithRelations = (user: User): Partial<UserDetailedNotMe> => {
+ const userDetailedNotMeWithRelations = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({
...userDetailedNotMe(user),
isFollowing: user.isFollowing ?? false,
@@ -128,7 +109,7 @@ describe('ユーザー', () => {
};
// MeDetailedのキーが過不足なく入っている?
- const meDetailed = (user: User, security = false): Partial<MeDetailed> => {
+ const meDetailed = (user: misskey.entities.SignupResponse, security = false): Partial<misskey.entities.MeDetailed> => {
return stripUndefined({
...userDetailedNotMe(user),
avatarId: user.avatarId,
@@ -159,6 +140,7 @@ describe('ユーザー', () => {
mutedWords: user.mutedWords,
hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances,
+ // @ts-expect-error 後方互換性
mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig,
emailNotificationTypes: user.emailNotificationTypes,
@@ -173,61 +155,53 @@ describe('ユーザー', () => {
});
};
- let root: User;
- let alice: User;
+ let root: misskey.entities.SignupResponse;
+ let alice: misskey.entities.SignupResponse;
let aliceNote: misskey.entities.Note;
- let alicePage: misskey.entities.Page;
- let aliceList: misskey.entities.UserList;
- let bob: User;
- let bobNote: misskey.entities.Note;
+ let bob: misskey.entities.SignupResponse;
+
+ // NOTE: これがないと落ちる(bob の updatedAt が null になってしまうため?)
+ let bobNote: misskey.entities.Note; // eslint-disable-line @typescript-eslint/no-unused-vars
- let carol: User;
- let dave: User;
- let ellen: User;
- let frank: User;
+ let carol: misskey.entities.SignupResponse;
- let usersReplying: User[];
+ let usersReplying: misskey.entities.SignupResponse[];
- let userNoNote: User;
- let userNotExplorable: User;
- let userLocking: User;
- let userAdmin: User;
- let roleAdmin: any;
- let userModerator: User;
- let roleModerator: any;
- let userRolePublic: User;
- let rolePublic: any;
- let userRoleBadge: User;
- let roleBadge: any;
- let userSilenced: User;
- let roleSilenced: any;
- let userSuspended: User;
- let userDeletedBySelf: User;
- let userDeletedByAdmin: User;
- let userFollowingAlice: User;
- let userFollowedByAlice: User;
- let userBlockingAlice: User;
- let userBlockedByAlice: User;
- let userMutingAlice: User;
- let userMutedByAlice: User;
- let userRnMutingAlice: User;
- let userRnMutedByAlice: User;
- let userFollowRequesting: User;
- let userFollowRequested: User;
+ let userNoNote: misskey.entities.SignupResponse;
+ let userNotExplorable: misskey.entities.SignupResponse;
+ let userLocking: misskey.entities.SignupResponse;
+ let userAdmin: misskey.entities.SignupResponse;
+ let roleAdmin: misskey.entities.Role;
+ let userModerator: misskey.entities.SignupResponse;
+ let roleModerator: misskey.entities.Role;
+ let userRolePublic: misskey.entities.SignupResponse;
+ let rolePublic: misskey.entities.Role;
+ let userRoleBadge: misskey.entities.SignupResponse;
+ let roleBadge: misskey.entities.Role;
+ let userSilenced: misskey.entities.SignupResponse;
+ let roleSilenced: misskey.entities.Role;
+ let userSuspended: misskey.entities.SignupResponse;
+ let userDeletedBySelf: misskey.entities.SignupResponse;
+ let userDeletedByAdmin: misskey.entities.SignupResponse;
+ let userFollowingAlice: misskey.entities.SignupResponse;
+ let userFollowedByAlice: misskey.entities.SignupResponse;
+ let userBlockingAlice: misskey.entities.SignupResponse;
+ let userBlockedByAlice: misskey.entities.SignupResponse;
+ let userMutingAlice: misskey.entities.SignupResponse;
+ let userMutedByAlice: misskey.entities.SignupResponse;
+ let userRnMutingAlice: misskey.entities.SignupResponse;
+ let userRnMutedByAlice: misskey.entities.SignupResponse;
+ let userFollowRequesting: misskey.entities.SignupResponse;
+ let userFollowRequested: misskey.entities.SignupResponse;
beforeAll(async () => {
root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' });
- aliceNote = await post(alice, { text: 'test' }) as any;
- alicePage = await page(alice);
- aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body;
+ aliceNote = await post(alice, { text: 'test' });
bob = await signup({ username: 'bob' });
- bobNote = await post(bob, { text: 'test' }) as any;
+ bobNote = await post(bob, { text: 'test' });
carol = await signup({ username: 'carol' });
- dave = await signup({ username: 'dave' });
- ellen = await signup({ username: 'ellen' });
- frank = await signup({ username: 'frank' });
// @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする
usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => {
@@ -238,7 +212,7 @@ describe('ユーザー', () => {
}
return (await acc).concat(u);
- }, Promise.resolve([] as User[]));
+ }, Promise.resolve([] as misskey.entities.SignupResponse[]));
userNoNote = await signup({ username: 'userNoNote' });
userNotExplorable = await signup({ username: 'userNotExplorable' });
@@ -306,7 +280,7 @@ describe('ユーザー', () => {
beforeEach(async () => {
alice = {
...alice,
- ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any,
+ ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }),
};
aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice });
});
@@ -319,7 +293,7 @@ describe('ユーザー', () => {
endpoint: 'signup',
parameters: { username: 'zoe', password: 'password' },
user: undefined,
- }) as unknown as User; // BUG MeDetailedに足りないキーがある
+ }) as unknown as misskey.entities.SignupResponse; // BUG MeDetailedに足りないキーがある
// signupの時はtokenが含まれる特別なMeDetailedが返ってくる
assert.match(response.token, /[a-zA-Z0-9]{16}/);
@@ -329,7 +303,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.name, null);
assert.strictEqual(response.username, 'zoe');
assert.strictEqual(response.host, null);
- assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
+ response.avatarUrl && assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.strictEqual(response.avatarBlurhash, null);
assert.deepStrictEqual(response.avatarDecorations, []);
assert.strictEqual(response.isBot, false);
@@ -401,6 +375,7 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.unreadAnnouncements, []);
assert.deepStrictEqual(response.mutedWords, []);
assert.deepStrictEqual(response.mutedInstances, []);
+ // @ts-expect-error 後方互換のため
assert.deepStrictEqual(response.mutingNotificationTypes, []);
assert.deepStrictEqual(response.notificationRecieveConfig, {});
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
@@ -430,66 +405,66 @@ describe('ユーザー', () => {
//#region 自分の情報の更新(i/update)
test.each([
- { parameters: (): object => ({ name: null }) },
- { parameters: (): object => ({ name: 'x'.repeat(50) }) },
- { parameters: (): object => ({ name: 'x' }) },
- { parameters: (): object => ({ name: 'My name' }) },
- { parameters: (): object => ({ description: null }) },
- { parameters: (): object => ({ description: 'x'.repeat(1500) }) },
- { parameters: (): object => ({ description: 'x' }) },
- { parameters: (): object => ({ description: 'My description' }) },
- { parameters: (): object => ({ location: null }) },
- { parameters: (): object => ({ location: 'x'.repeat(50) }) },
- { parameters: (): object => ({ location: 'x' }) },
- { parameters: (): object => ({ location: 'My location' }) },
- { parameters: (): object => ({ birthday: '0000-00-00' }) },
- { parameters: (): object => ({ birthday: '9999-99-99' }) },
- { parameters: (): object => ({ lang: 'en-US' }) },
- { parameters: (): object => ({ fields: [] }) },
- { parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) },
- { parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
- { parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
- { parameters: (): object => ({ isLocked: true }) },
- { parameters: (): object => ({ isLocked: false }) },
- { parameters: (): object => ({ isExplorable: false }) },
- { parameters: (): object => ({ isExplorable: true }) },
- { parameters: (): object => ({ hideOnlineStatus: true }) },
- { parameters: (): object => ({ hideOnlineStatus: false }) },
- { parameters: (): object => ({ publicReactions: false }) },
- { parameters: (): object => ({ publicReactions: true }) },
- { parameters: (): object => ({ autoAcceptFollowed: true }) },
- { parameters: (): object => ({ autoAcceptFollowed: false }) },
- { parameters: (): object => ({ noCrawle: true }) },
- { parameters: (): object => ({ noCrawle: false }) },
- { parameters: (): object => ({ preventAiLearning: false }) },
- { parameters: (): object => ({ preventAiLearning: true }) },
- { parameters: (): object => ({ isBot: true }) },
- { parameters: (): object => ({ isBot: false }) },
- { parameters: (): object => ({ isCat: true }) },
- { parameters: (): object => ({ isCat: false }) },
- { parameters: (): object => ({ injectFeaturedNote: true }) },
- { parameters: (): object => ({ injectFeaturedNote: false }) },
- { parameters: (): object => ({ receiveAnnouncementEmail: true }) },
- { parameters: (): object => ({ receiveAnnouncementEmail: false }) },
- { parameters: (): object => ({ alwaysMarkNsfw: true }) },
- { parameters: (): object => ({ alwaysMarkNsfw: false }) },
- { parameters: (): object => ({ autoSensitive: true }) },
- { parameters: (): object => ({ autoSensitive: false }) },
- { parameters: (): object => ({ followingVisibility: 'private' }) },
- { parameters: (): object => ({ followingVisibility: 'followers' }) },
- { parameters: (): object => ({ followingVisibility: 'public' }) },
- { parameters: (): object => ({ followersVisibility: 'private' }) },
- { parameters: (): object => ({ followersVisibility: 'followers' }) },
- { parameters: (): object => ({ followersVisibility: 'public' }) },
- { parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
- { parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) },
- { parameters: (): object => ({ mutedWords: [] }) },
- { parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
- { parameters: (): object => ({ mutedInstances: [] }) },
- { parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
- { parameters: (): object => ({ notificationRecieveConfig: {} }) },
- { parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
- { parameters: (): object => ({ emailNotificationTypes: [] }) },
+ { parameters: () => ({ name: null }) },
+ { parameters: () => ({ name: 'x'.repeat(50) }) },
+ { parameters: () => ({ name: 'x' }) },
+ { parameters: () => ({ name: 'My name' }) },
+ { parameters: () => ({ description: null }) },
+ { parameters: () => ({ description: 'x'.repeat(1500) }) },
+ { parameters: () => ({ description: 'x' }) },
+ { parameters: () => ({ description: 'My description' }) },
+ { parameters: () => ({ location: null }) },
+ { parameters: () => ({ location: 'x'.repeat(50) }) },
+ { parameters: () => ({ location: 'x' }) },
+ { parameters: () => ({ location: 'My location' }) },
+ { parameters: () => ({ birthday: '0000-00-00' }) },
+ { parameters: () => ({ birthday: '9999-99-99' }) },
+ { parameters: () => ({ lang: 'en-US' as const }) },
+ { parameters: () => ({ fields: [] }) },
+ { parameters: () => ({ fields: [{ name: 'x', value: 'x' }] }) },
+ { parameters: () => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
+ { parameters: () => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
+ { parameters: () => ({ isLocked: true }) },
+ { parameters: () => ({ isLocked: false }) },
+ { parameters: () => ({ isExplorable: false }) },
+ { parameters: () => ({ isExplorable: true }) },
+ { parameters: () => ({ hideOnlineStatus: true }) },
+ { parameters: () => ({ hideOnlineStatus: false }) },
+ { parameters: () => ({ publicReactions: false }) },
+ { parameters: () => ({ publicReactions: true }) },
+ { parameters: () => ({ autoAcceptFollowed: true }) },
+ { parameters: () => ({ autoAcceptFollowed: false }) },
+ { parameters: () => ({ noCrawle: true }) },
+ { parameters: () => ({ noCrawle: false }) },
+ { parameters: () => ({ preventAiLearning: false }) },
+ { parameters: () => ({ preventAiLearning: true }) },
+ { parameters: () => ({ isBot: true }) },
+ { parameters: () => ({ isBot: false }) },
+ { parameters: () => ({ isCat: true }) },
+ { parameters: () => ({ isCat: false }) },
+ { parameters: () => ({ injectFeaturedNote: true }) },
+ { parameters: () => ({ injectFeaturedNote: false }) },
+ { parameters: () => ({ receiveAnnouncementEmail: true }) },
+ { parameters: () => ({ receiveAnnouncementEmail: false }) },
+ { parameters: () => ({ alwaysMarkNsfw: true }) },
+ { parameters: () => ({ alwaysMarkNsfw: false }) },
+ { parameters: () => ({ autoSensitive: true }) },
+ { parameters: () => ({ autoSensitive: false }) },
+ { parameters: () => ({ followingVisibility: 'private' as const }) },
+ { parameters: () => ({ followingVisibility: 'followers' as const }) },
+ { parameters: () => ({ followingVisibility: 'public' as const }) },
+ { parameters: () => ({ followersVisibility: 'private' as const }) },
+ { parameters: () => ({ followersVisibility: 'followers' as const }) },
+ { parameters: () => ({ followersVisibility: 'public' as const }) },
+ { parameters: () => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
+ { parameters: () => ({ mutedWords: [['x'.repeat(194)]] }) },
+ { parameters: () => ({ mutedWords: [] }) },
+ { parameters: () => ({ mutedInstances: ['xxxx.xxxxx'] }) },
+ { parameters: () => ({ mutedInstances: [] }) },
+ { parameters: () => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
+ { parameters: () => ({ notificationRecieveConfig: {} }) },
+ { parameters: () => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
+ { parameters: () => ({ emailNotificationTypes: [] }) },
] as const)('を書き換えることができる($#)', async ({ parameters }) => {
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice });
const expected = { ...meDetailed(alice, true), ...parameters() };
@@ -498,13 +473,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Avatar)', async () => {
const aliceFile = (await uploadFile(alice)).body;
- const parameters = { avatarId: aliceFile.id };
+ const parameters = { avatarId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/);
const expected = {
...meDetailed(alice, true),
- avatarId: aliceFile.id,
+ avatarId: aliceFile!.id,
avatarBlurhash: response.avatarBlurhash,
avatarUrl: response.avatarUrl,
};
@@ -523,13 +498,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Banner)', async () => {
const aliceFile = (await uploadFile(alice)).body;
- const parameters = { bannerId: aliceFile.id };
+ const parameters = { bannerId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/);
const expected = {
...meDetailed(alice, true),
- bannerId: aliceFile.id,
+ bannerId: aliceFile!.id,
bannerBlurhash: response.bannerBlurhash,
bannerUrl: response.bannerUrl,
};
@@ -579,13 +554,13 @@ describe('ユーザー', () => {
//#region ユーザー(users)
test.each([
- { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id },
- { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
- { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
- { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
- { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
- { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
- { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
+ { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: misskey.entities.UserLite): string => u.id },
+ { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
+ { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
+ { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
+ { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
+ { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
+ { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をリスト形式で取得することができる($label)', async ({ parameters, selector }) => {
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@@ -598,15 +573,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
- { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true },
- { label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true },
- { label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true },
- { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true },
- { label: '承認制ユーザーが含まれる', user: (): User => userLocking },
- { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
- { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
- { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
- { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
+ { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: () => userNotExplorable, excluded: true },
+ { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true },
+ { label: 'ブロックされているユーザーが含まれない', user: () => userBlockedByAlice, excluded: true },
+ { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice, excluded: true },
+ { label: '承認制ユーザーが含まれる', user: () => userLocking },
+ { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
+ { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
+ { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
+ { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => {
const parameters = { limit: 100 };
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@@ -620,39 +595,44 @@ describe('ユーザー', () => {
//#region ユーザー情報(users/show)
test.each([
- { label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed },
- { label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations },
- { label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
- { label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed },
- { label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations },
- { label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
+ { label: 'ID指定で自分自身を', parameters: () => ({ userId: alice.id }), user: () => alice, type: meDetailed },
+ { label: 'ID指定で他人を', parameters: () => ({ userId: alice.id }), user: () => bob, type: userDetailedNotMeWithRelations },
+ { label: 'ID指定かつ未認証', parameters: () => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
+ { label: '@指定で自分自身を', parameters: () => ({ username: alice.username }), user: () => alice, type: meDetailed },
+ { label: '@指定で他人を', parameters: () => ({ username: alice.username }), user: () => bob, type: userDetailedNotMeWithRelations },
+ { label: '@指定かつ未認証', parameters: () => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
] as const)('を取得することができる($label)', async ({ parameters, user, type }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() });
const expected = type(alice);
assert.deepStrictEqual(response, expected);
});
test.each([
- { label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin },
- { label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined },
- { label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator },
- { label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined },
- { label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced },
- //{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended },
- { label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted },
- { label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
- { label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted },
- { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
- { label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing },
- { label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed },
- { label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking },
- { label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked },
- { label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted },
- { label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted },
- { label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou },
- { label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou },
+ { label: 'Administratorになっている', user: () => userAdmin, me: () => userAdmin, selector: (user: misskey.entities.MeDetailed) => user.isAdmin },
+ // @ts-expect-error UserDetailedNotMe doesn't include isAdmin
+ { label: '自分以外から見たときはAdministratorか判定できない', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined },
+ { label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
+ // @ts-expect-error UserDetailedNotMe doesn't include isModerator
+ { label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
+ { label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
+ // FIXME: 落ちる
+ //{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
+ { label: '削除済みになっている', user: () => userDeletedBySelf, me: () => userDeletedBySelf, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
+ // @ts-expect-error UserDetailedNotMe doesn't include isDeleted
+ { label: '自分以外から見たときは削除済みか判定できない', user: () => userDeletedBySelf, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
+ { label: '削除済み(byAdmin)になっている', user: () => userDeletedByAdmin, me: () => userDeletedByAdmin, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
+ // @ts-expect-error UserDetailedNotMe doesn't include isDeleted
+ { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: () => userDeletedByAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
+ { label: 'フォロー中になっている', user: () => userFollowedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowing },
+ { label: 'フォローされている', user: () => userFollowingAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowed },
+ { label: 'ブロック中になっている', user: () => userBlockedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocking },
+ { label: 'ブロックされている', user: () => userBlockingAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocked },
+ { label: 'ミュート中になっている', user: () => userMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isMuted },
+ { label: 'リノートミュート中になっている', user: () => userRnMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isRenoteMuted },
+ { label: 'フォローリクエスト中になっている', user: () => userFollowRequested, me: () => userFollowRequesting, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestFromYou },
+ { label: 'フォローリクエストされている', user: () => userFollowRequesting, me: () => userFollowRequested, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestToYou },
] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice });
- assert.strictEqual(selector(response), (expected ?? ((): true => true))());
+ assert.strictEqual(selector(response as any), (expected ?? ((): true => true))());
});
test('を取得することができ、Publicなロールがセットされていること', async () => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice });
@@ -694,17 +674,18 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
- { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
- { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
- { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
- { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
- { label: '承認制ユーザーが含まれる', user: (): User => userLocking },
- { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
- { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root },
+ { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
+ { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
+ { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
+ { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
+ { label: '承認制ユーザーが含まれる', user: () => userLocking },
+ { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
+ { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: () => userSuspended, me: () => root },
// BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる
- //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true },
- { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
- { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
+ //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: () => userSuspended, me: () => bob, excluded: true },
+ { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
+ { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
+ // @ts-expect-error excluded は上でコメントアウトされているので
] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => {
const parameters = { userIds: [user().id] };
const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice });
@@ -729,15 +710,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
- { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
- { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
- { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
- { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
- { label: '承認制ユーザーが含まれる', user: (): User => userLocking },
- { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
- { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
- { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
- { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
+ { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
+ { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
+ { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
+ { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
+ { label: '承認制ユーザーが含まれる', user: () => userLocking },
+ { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
+ { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
+ { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
+ { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => {
const parameters = { query: user().username, limit: 1 };
const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
@@ -751,30 +732,30 @@ describe('ユーザー', () => {
//#region ID指定検索(users/search-by-username-and-host)
test.each([
- { label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] },
- { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] },
- { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] },
- { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] },
- { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] },
- { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] },
- { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] },
- { label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] },
- { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] },
+ { label: '自分', parameters: { username: 'alice' }, user: () => [alice] },
+ { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: () => [alice] },
+ { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: () => [userFollowedByAlice] },
+ { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: () => [] },
+ { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: () => [bob] },
+ { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: () => [bob] },
+ { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: () => [bob] },
+ { label: 'ローカル', parameters: { host: null, limit: 1 }, user: () => [userFollowedByAlice] },
+ { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: () => [userFollowedByAlice] },
])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => {
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
const expected = await Promise.all(user().map(u => show(u.id, alice)));
assert.deepStrictEqual(response, expected);
});
test.each([
- { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
- { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
- { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
- { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
- { label: '承認制ユーザーが含まれる', user: (): User => userLocking },
- { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
- { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
- { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
- { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
+ { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
+ { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
+ { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
+ { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
+ { label: '承認制ユーザーが含まれる', user: () => userLocking },
+ { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
+ { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
+ { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
+ { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => {
const parameters = { username: user().username };
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
@@ -796,15 +777,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
- { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
- { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
- { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
- { label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true },
- { label: '承認制ユーザーが含まれる', user: (): User => userLocking },
- { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
- //{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
- { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
- { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
+ { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
+ { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
+ { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
+ { label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true },
+ { label: '承認制ユーザーが含まれる', user: () => userLocking },
+ { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
+ //{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
+ { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
+ { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => {
const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0];
await post(alice, { text: `@${user().username} test`, replyId: replyTo.id });
@@ -818,12 +799,12 @@ describe('ユーザー', () => {
//#region ハッシュタグ(hashtags/users)
test.each([
- { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
- { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
- { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
- { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
- { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
- { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
+ { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
+ { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
+ { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
+ { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
+ { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
+ { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => {
const hashtag = 'test_hashtag';
await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice });
@@ -837,15 +818,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected);
});
test.each([
- { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
- { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
- { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
- { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
- { label: '承認制ユーザーが含まれる', user: (): User => userLocking },
- { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
- { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
- { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
- { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
+ { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
+ { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
+ { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
+ { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
+ { label: '承認制ユーザーが含まれる', user: () => userLocking },
+ { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
+ { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
+ { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
+ { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => {
const hashtag = `user_test${user().username}`;
if (user() !== userSuspended) {
diff --git a/packages/backend/test/global.d.ts b/packages/backend/test/global.d.ts
new file mode 100644
index 0000000000..0363073356
--- /dev/null
+++ b/packages/backend/test/global.d.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type FIXME = any;
diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts
index cf5b9bf24d..861bc6db66 100644
--- a/packages/backend/test/jest.setup.ts
+++ b/packages/backend/test/jest.setup.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { initTestDb, sendEnvResetRequest } from './utils.js';
beforeAll(async () => {
diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts
index b86a7a978d..7aa7a92702 100644
--- a/packages/backend/test/prelude/get-api-validator.ts
+++ b/packages/backend/test/prelude/get-api-validator.ts
@@ -4,10 +4,10 @@
*/
import Ajv from 'ajv';
-import { Schema } from '@/misc/schema';
+import { Schema } from '@/misc/json-schema.js';
export const getValidator = (paramDef: Schema) => {
- const ajv = new Ajv({
+ const ajv = new Ajv.default({
useDefaults: true,
});
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
diff --git a/packages/backend/test/resources/kick_gaba7.m4a b/packages/backend/test/resources/kick_gaba7.m4a
new file mode 100644
index 0000000000..321df6349f
--- /dev/null
+++ b/packages/backend/test/resources/kick_gaba7.m4a
Binary files differ
diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json
index 4597ff8780..2b562acda8 100644
--- a/packages/backend/test/tsconfig.json
+++ b/packages/backend/test/tsconfig.json
@@ -5,7 +5,7 @@
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedParameters": false,
- "noUnusedLocals": true,
+ "noUnusedLocals": false,
"noFallthroughCasesInSwitch": true,
"declaration": false,
"sourceMap": true,
@@ -18,6 +18,7 @@
"strict": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
+ "skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts
index fc35837420..81da0fac31 100644
--- a/packages/backend/test/unit/AnnouncementService.ts
+++ b/packages/backend/test/unit/AnnouncementService.ts
@@ -10,6 +10,7 @@ import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import type {
AnnouncementReadsRepository,
AnnouncementsRepository,
@@ -51,7 +52,7 @@ describe('AnnouncementService', () => {
function createAnnouncement(data: Partial<MiAnnouncement & { createdAt: Date }> = {}) {
return announcementsRepository.insert({
- id: genAidx(data.createdAt ?? new Date()),
+ id: genAidx(data.createdAt?.getTime() ?? Date.now()),
updatedAt: null,
title: 'Title',
text: 'Text',
@@ -67,6 +68,7 @@ describe('AnnouncementService', () => {
],
providers: [
AnnouncementService,
+ AnnouncementEntityService,
CacheService,
IdService,
],
diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts
index 2b79041c86..79cb81f5c9 100644
--- a/packages/backend/test/unit/ApMfmService.ts
+++ b/packages/backend/test/unit/ApMfmService.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import * as assert from 'assert';
import { Test } from '@nestjs/testing';
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index e6e68ccd6d..bf8f3ab0e3 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -19,8 +19,8 @@ import { DI } from '@/di-symbols.js';
import type { TestingModule } from '@nestjs/testing';
function mockRedis() {
- const hash = {};
- const set = jest.fn((key, value) => {
+ const hash = {} as any;
+ const set = jest.fn((key: string, value) => {
const ret = hash[key];
hash[key] = value;
return ret;
@@ -56,12 +56,13 @@ describe('FetchInstanceMetadataService', () => {
} else if (token === DI.redis) {
return mockRedis;
}
+ return null;
})
.compile();
app.enableShutdownHooks();
- fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService);
+ fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService) as jest.Mocked<FetchInstanceMetadataService>;
federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService) as jest.Mocked<FederatedInstanceService>;
redisClient = app.get<Redis>(DI.redis) as jest.Mocked<Redis>;
httpRequestService = app.get<HttpRequestService>(HttpRequestService) as jest.Mocked<HttpRequestService>;
@@ -74,11 +75,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } });
+ federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
- await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
+
+ await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@@ -88,11 +90,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and don\'t update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now } });
+ federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
- await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
+
+ await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@@ -101,15 +104,33 @@ describe('FetchInstanceMetadataService', () => {
test('Do nothing when lock not acquired', async () => {
redisClient.set = mockRedis();
- federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } });
+ const now = Date.now();
+ federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
+ await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
- await fetchInstanceMetadataService.tryLock('example.com');
- await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
- expect(tryLockSpy).toHaveBeenCalledTimes(2);
+
+ await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
+ expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(0);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
});
+
+ test('Do when lock not acquired but forced', async () => {
+ redisClient.set = mockRedis();
+ const now = Date.now();
+ federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+ httpRequestService.getJson.mockImplementation(() => { throw Error(); });
+ await fetchInstanceMetadataService.tryLock('example.com');
+ const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
+ const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
+
+ await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
+ expect(tryLockSpy).toHaveBeenCalledTimes(0);
+ expect(unlockSpy).toHaveBeenCalledTimes(1);
+ expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+ expect(httpRequestService.getJson).toHaveBeenCalled();
+ });
});
diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts
index 2eec80d763..40d187f5a8 100644
--- a/packages/backend/test/unit/FileInfoService.ts
+++ b/packages/backend/test/unit/FileInfoService.ts
@@ -15,6 +15,7 @@ import { GlobalModule } from '@/GlobalModule.js';
import { FileInfoService } from '@/core/FileInfoService.js';
//import { DI } from '@/di-symbols.js';
import { AiService } from '@/core/AiService.js';
+import { LoggerService } from '@/core/LoggerService.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
@@ -35,6 +36,7 @@ describe('FileInfoService', () => {
],
providers: [
AiService,
+ LoggerService,
FileInfoService,
],
})
@@ -323,8 +325,26 @@ describe('FileInfoService', () => {
});
});
- /*
- * video/webmとして検出されてしまう
+ test('MPEG-4 AUDIO (M4A)', async () => {
+ const path = `${resources}/kick_gaba7.m4a`;
+ const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
+ delete info.warnings;
+ delete info.blurhash;
+ delete info.sensitive;
+ delete info.porn;
+ delete info.width;
+ delete info.height;
+ delete info.orientation;
+ assert.deepStrictEqual(info, {
+ size: 9817,
+ md5: '74c9279a4abe98789565f1dc1a541a42',
+ type: {
+ mime: 'audio/mp4',
+ ext: 'm4a',
+ },
+ });
+ });
+
test('WEBM AUDIO', async () => {
const path = `${resources}/kick_gaba7.webm`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@@ -337,13 +357,12 @@ describe('FileInfoService', () => {
delete info.orientation;
assert.deepStrictEqual(info, {
size: 8879,
- md5: '3350083dec312419cfdc06c16413aca7',
+ md5: '53bc1adcb6acbbda67ff9bd484896438',
type: {
mime: 'audio/webm',
ext: 'webm',
},
});
});
- */
});
});
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index f613fe9c7c..fd4a03413b 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -39,6 +39,12 @@ describe('MfmService', () => {
const output = '<p>foo <i>bar</i></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
+
+ test('escape', () => {
+ const input = '```\n<p>Hello, world!</p>\n```';
+ const output = '<p><pre><code>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre></p>';
+ assert.equal(mfmService.toHtml(mfm.parse(input)), output);
+ });
});
describe('fromHtml', () => {
diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts
new file mode 100644
index 0000000000..f2d4c8ffbb
--- /dev/null
+++ b/packages/backend/test/unit/NoteCreateService.ts
@@ -0,0 +1,144 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test } from '@nestjs/testing';
+
+import { CoreModule } from '@/core/CoreModule.js';
+import { NoteCreateService } from '@/core/NoteCreateService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { MiNote } from '@/models/Note.js';
+import { IPoll } from '@/models/Poll.js';
+import { MiDriveFile } from '@/models/DriveFile.js';
+
+describe('NoteCreateService', () => {
+ let noteCreateService: NoteCreateService;
+
+ beforeAll(async () => {
+ const app = await Test.createTestingModule({
+ imports: [GlobalModule, CoreModule],
+ }).compile();
+ noteCreateService = app.get<NoteCreateService>(NoteCreateService);
+ });
+
+ describe('is-renote', () => {
+ const base: MiNote = {
+ id: 'some-note-id',
+ replyId: null,
+ reply: null,
+ renoteId: null,
+ renote: null,
+ threadId: null,
+ text: null,
+ name: null,
+ cw: null,
+ userId: 'some-user-id',
+ user: null,
+ localOnly: false,
+ reactionAcceptance: null,
+ renoteCount: 0,
+ repliesCount: 0,
+ clippedCount: 0,
+ reactions: {},
+ visibility: 'public',
+ uri: null,
+ url: null,
+ fileIds: [],
+ attachedFileTypes: [],
+ visibleUserIds: [],
+ mentions: [],
+ mentionedRemoteUsers: '',
+ reactionAndUserPairCache: [],
+ emojis: [],
+ tags: [],
+ hasPoll: false,
+ channelId: null,
+ channel: null,
+ userHost: null,
+ replyUserId: null,
+ replyUserHost: null,
+ renoteUserId: null,
+ renoteUserHost: null,
+ };
+
+ const poll: IPoll = {
+ choices: ['kinoko', 'takenoko'],
+ multiple: false,
+ expiresAt: null,
+ };
+
+ const file: MiDriveFile = {
+ id: 'some-file-id',
+ userId: null,
+ user: null,
+ userHost: null,
+ md5: '',
+ name: '',
+ type: '',
+ size: 0,
+ comment: null,
+ blurhash: null,
+ properties: {},
+ storedInternal: false,
+ url: '',
+ thumbnailUrl: null,
+ webpublicUrl: null,
+ webpublicType: null,
+ accessKey: null,
+ thumbnailAccessKey: null,
+ webpublicAccessKey: null,
+ uri: null,
+ src: null,
+ folderId: null,
+ folder: null,
+ isSensitive: false,
+ maybeSensitive: false,
+ maybePorn: false,
+ isLink: false,
+ requestHeaders: null,
+ requestIp: null,
+ };
+
+ test('note without renote should not be Renote', () => {
+ const note = { renote: null };
+ expect(noteCreateService['isRenote'](note)).toBe(false);
+ });
+
+ test('note with renote should be Renote and not be Quote', () => {
+ const note = { renote: base };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(false);
+ });
+
+ test('note with renote and text should be Quote', () => {
+ const note = { renote: base, text: 'some-text' };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+
+ test('note with renote and cw should be Quote', () => {
+ const note = { renote: base, cw: 'some-cw' };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+
+ test('note with renote and reply should be Quote', () => {
+ const note = { renote: base, reply: { ...base, id: 'another-note-id' } };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+
+ test('note with renote and poll should be Quote', () => {
+ const note = { renote: base, poll };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+
+ test('note with renote and non-empty files should be Quote', () => {
+ const note = { renote: base, files: [file] };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+ });
+});
diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts
index f2a67dba46..9676abf07b 100644
--- a/packages/backend/test/unit/RelayService.ts
+++ b/packages/backend/test/unit/RelayService.ts
@@ -90,7 +90,8 @@ describe('RelayService', () => {
expect(queueService.deliver).toHaveBeenCalled();
expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo');
- expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow');
+ expect(typeof queueService.deliver.mock.lastCall![1]?.object).toBe('object');
+ expect((queueService.deliver.mock.lastCall![1]?.object as any).type).toBe('Follow');
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index fe5ad31597..ec441735d7 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+
process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
@@ -20,6 +22,7 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
+import { RoleCondFormulaValue } from '@/models/Role.js';
import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
@@ -52,12 +55,26 @@ describe('RoleService', () => {
id: genAidx(Date.now()),
updatedAt: new Date(),
lastUsedAt: new Date(),
+ name: '',
description: '',
...data,
})
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
}
+ function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial<MiRole> = {}) {
+ return createRole({
+ name: `[conditional] ${condFormula.type}`,
+ target: 'conditional',
+ condFormula: condFormula,
+ ...data,
+ });
+ }
+
+ function aidx() {
+ return genAidx(Date.now());
+ }
+
beforeEach(async () => {
clock = lolex.install({
now: new Date(),
@@ -73,6 +90,7 @@ describe('RoleService', () => {
CacheService,
IdService,
GlobalEventService,
+ UserEntityService,
{
provide: NotificationService,
useFactory: () => ({
@@ -209,15 +227,9 @@ describe('RoleService', () => {
expect(result.driveCapacityMb).toBe(100);
});
- test('conditional role', async () => {
- const user1 = await createUser({
- id: genAidx(Date.now() - (1000 * 60 * 60 * 24 * 365)),
- });
- const user2 = await createUser({
- id: genAidx(Date.now() - (1000 * 60 * 60 * 24 * 365)),
- followersCount: 10,
- });
- await createRole({
+ test('expired role', async () => {
+ const user = await createUser();
+ const role = await createRole({
name: 'a',
policies: {
canManageCustomEmojis: {
@@ -226,32 +238,133 @@ describe('RoleService', () => {
value: true,
},
},
- target: 'conditional',
- condFormula: {
- type: 'and',
- values: [{
- type: 'followersMoreThanOrEq',
- value: 10,
- }, {
- type: 'createdMoreThan',
- sec: 60 * 60 * 24 * 7,
- }],
- },
});
-
+ await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24)));
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: false,
},
} as any);
- const user1Policies = await roleService.getUserPolicies(user1.id);
- const user2Policies = await roleService.getUserPolicies(user2.id);
- expect(user1Policies.canManageCustomEmojis).toBe(false);
- expect(user2Policies.canManageCustomEmojis).toBe(true);
+ const result = await roleService.getUserPolicies(user.id);
+ expect(result.canManageCustomEmojis).toBe(true);
+
+ clock.tick('25:00:00');
+
+ const resultAfter25h = await roleService.getUserPolicies(user.id);
+ expect(resultAfter25h.canManageCustomEmojis).toBe(false);
+
+ await roleService.assign(user.id, role.id);
+
+ // ストリーミング経由で反映されるまでちょっと待つ
+ clock.uninstall();
+ await sleep(100);
+
+ const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
+ expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
+ });
+ });
+
+ describe('conditional role', () => {
+ test('~かつ~', async () => {
+ const [user1, user2, user3, user4] = await Promise.all([
+ createUser({ isBot: true, isCat: false, isSuspended: false }),
+ createUser({ isBot: false, isCat: true, isSuspended: false }),
+ createUser({ isBot: true, isCat: true, isSuspended: false }),
+ createUser({ isBot: false, isCat: false, isSuspended: true }),
+ ]);
+ const role1 = await createConditionalRole({
+ id: aidx(),
+ type: 'isBot',
+ });
+ const role2 = await createConditionalRole({
+ id: aidx(),
+ type: 'isCat',
+ });
+ const role3 = await createConditionalRole({
+ id: aidx(),
+ type: 'isSuspended',
+ });
+ const role4 = await createConditionalRole({
+ id: aidx(),
+ type: 'and',
+ values: [role1.condFormula, role2.condFormula],
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ const actual4 = await roleService.getUserRoles(user4.id);
+ expect(actual1.some(r => r.id === role4.id)).toBe(false);
+ expect(actual2.some(r => r.id === role4.id)).toBe(false);
+ expect(actual3.some(r => r.id === role4.id)).toBe(true);
+ expect(actual4.some(r => r.id === role4.id)).toBe(false);
+ });
+
+ test('~または~', async () => {
+ const [user1, user2, user3, user4] = await Promise.all([
+ createUser({ isBot: true, isCat: false, isSuspended: false }),
+ createUser({ isBot: false, isCat: true, isSuspended: false }),
+ createUser({ isBot: true, isCat: true, isSuspended: false }),
+ createUser({ isBot: false, isCat: false, isSuspended: true }),
+ ]);
+ const role1 = await createConditionalRole({
+ id: aidx(),
+ type: 'isBot',
+ });
+ const role2 = await createConditionalRole({
+ id: aidx(),
+ type: 'isCat',
+ });
+ const role3 = await createConditionalRole({
+ id: aidx(),
+ type: 'isSuspended',
+ });
+ const role4 = await createConditionalRole({
+ id: aidx(),
+ type: 'or',
+ values: [role1.condFormula, role2.condFormula],
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ const actual4 = await roleService.getUserRoles(user4.id);
+ expect(actual1.some(r => r.id === role4.id)).toBe(true);
+ expect(actual2.some(r => r.id === role4.id)).toBe(true);
+ expect(actual3.some(r => r.id === role4.id)).toBe(true);
+ expect(actual4.some(r => r.id === role4.id)).toBe(false);
+ });
+
+ test('~ではない', async () => {
+ const [user1, user2, user3] = await Promise.all([
+ createUser({ isBot: true, isCat: false, isSuspended: false }),
+ createUser({ isBot: false, isCat: true, isSuspended: false }),
+ createUser({ isBot: true, isCat: true, isSuspended: false }),
+ ]);
+ const role1 = await createConditionalRole({
+ id: aidx(),
+ type: 'isBot',
+ });
+ const role2 = await createConditionalRole({
+ id: aidx(),
+ type: 'isCat',
+ });
+ const role4 = await createConditionalRole({
+ id: aidx(),
+ type: 'not',
+ value: role1.condFormula,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role4.id)).toBe(false);
+ expect(actual2.some(r => r.id === role4.id)).toBe(true);
+ expect(actual3.some(r => r.id === role4.id)).toBe(false);
});
- test('コンディショナルロール: マニュアルロールにアサイン済み', async () => {
+ test('マニュアルロールにアサイン済み', async () => {
const [user1, user2, role1] = await Promise.all([
createUser(),
createUser(),
@@ -259,15 +372,10 @@ describe('RoleService', () => {
name: 'manual role',
}),
]);
- const role2 = await createRole({
- name: 'conditional role',
- target: 'conditional',
- condFormula: {
- // idはバックエンドのロジックに必要ない?
- id: 'bdc612bd-9d54-4675-ae83-0499c82ea670',
- type: 'roleAssignedTo',
- roleId: role1.id,
- },
+ const role2 = await createConditionalRole({
+ id: aidx(),
+ type: 'roleAssignedTo',
+ roleId: role1.id,
});
await roleService.assign(user2.id, role1.id);
@@ -279,41 +387,302 @@ describe('RoleService', () => {
expect(u2role.some(r => r.id === role2.id)).toBe(true);
});
- test('expired role', async () => {
- const user = await createUser();
- const role = await createRole({
- name: 'a',
- policies: {
- canManageCustomEmojis: {
- useDefault: false,
- priority: 0,
- value: true,
- },
- },
+ test('ローカルユーザのみ', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ host: null }),
+ createUser({ host: 'example.com' }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isLocal',
});
- await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24)));
- metaService.fetch.mockResolvedValue({
- policies: {
- canManageCustomEmojis: false,
- },
- } as any);
- const result = await roleService.getUserPolicies(user.id);
- expect(result.canManageCustomEmojis).toBe(true);
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(true);
+ expect(actual2.some(r => r.id === role.id)).toBe(false);
+ });
- clock.tick('25:00:00');
+ test('リモートユーザのみ', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ host: null }),
+ createUser({ host: 'example.com' }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isRemote',
+ });
- const resultAfter25h = await roleService.getUserPolicies(user.id);
- expect(resultAfter25h.canManageCustomEmojis).toBe(false);
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ });
- await roleService.assign(user.id, role.id);
+ test('サスペンド済みユーザである', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ isSuspended: false }),
+ createUser({ isSuspended: true }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isSuspended',
+ });
- // ストリーミング経由で反映されるまでちょっと待つ
- clock.uninstall();
- await sleep(100);
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ });
- const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
- expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
+ test('鍵アカウントユーザである', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ isLocked: false }),
+ createUser({ isLocked: true }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isLocked',
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ });
+
+ test('botユーザである', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ isBot: false }),
+ createUser({ isBot: true }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isBot',
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ });
+
+ test('猫である', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ isCat: false }),
+ createUser({ isCat: true }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isCat',
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ });
+
+ test('「ユーザを見つけやすくする」が有効なアカウント', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ isExplorable: false }),
+ createUser({ isExplorable: true }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isExplorable',
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ });
+
+ test('ユーザが作成されてから指定期間経過した', async () => {
+ const base = new Date();
+ base.setMinutes(base.getMinutes() - 5);
+
+ const d1 = new Date(base);
+ const d2 = new Date(base);
+ const d3 = new Date(base);
+ d1.setSeconds(d1.getSeconds() - 1);
+ d3.setSeconds(d3.getSeconds() + 1);
+
+ const [user1, user2, user3] = await Promise.all([
+ // 4:59
+ createUser({ id: genAidx(d1.getTime()) }),
+ // 5:00
+ createUser({ id: genAidx(d2.getTime()) }),
+ // 5:01
+ createUser({ id: genAidx(d3.getTime()) }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'createdLessThan',
+ // 5 minutes
+ sec: 300,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(false);
+ expect(actual3.some(r => r.id === role.id)).toBe(true);
+ });
+
+ test('ユーザが作成されてから指定期間経っていない', async () => {
+ const base = new Date();
+ base.setMinutes(base.getMinutes() - 5);
+
+ const d1 = new Date(base);
+ const d2 = new Date(base);
+ const d3 = new Date(base);
+ d1.setSeconds(d1.getSeconds() - 1);
+ d3.setSeconds(d3.getSeconds() + 1);
+
+ const [user1, user2, user3] = await Promise.all([
+ // 4:59
+ createUser({ id: genAidx(d1.getTime()) }),
+ // 5:00
+ createUser({ id: genAidx(d2.getTime()) }),
+ // 5:01
+ createUser({ id: genAidx(d3.getTime()) }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'createdMoreThan',
+ // 5 minutes
+ sec: 300,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(true);
+ expect(actual2.some(r => r.id === role.id)).toBe(false);
+ expect(actual3.some(r => r.id === role.id)).toBe(false);
+ });
+
+ test('フォロワー数が指定値以下', async () => {
+ const [user1, user2, user3] = await Promise.all([
+ createUser({ followersCount: 99 }),
+ createUser({ followersCount: 100 }),
+ createUser({ followersCount: 101 }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'followersLessThanOrEq',
+ value: 100,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(true);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ expect(actual3.some(r => r.id === role.id)).toBe(false);
+ });
+
+ test('フォロワー数が指定値以下', async () => {
+ const [user1, user2, user3] = await Promise.all([
+ createUser({ followersCount: 99 }),
+ createUser({ followersCount: 100 }),
+ createUser({ followersCount: 101 }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'followersMoreThanOrEq',
+ value: 100,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ expect(actual3.some(r => r.id === role.id)).toBe(true);
+ });
+
+ test('フォロー数が指定値以下', async () => {
+ const [user1, user2, user3] = await Promise.all([
+ createUser({ followingCount: 99 }),
+ createUser({ followingCount: 100 }),
+ createUser({ followingCount: 101 }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'followingLessThanOrEq',
+ value: 100,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(true);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ expect(actual3.some(r => r.id === role.id)).toBe(false);
+ });
+
+ test('フォロー数が指定値以上', async () => {
+ const [user1, user2, user3] = await Promise.all([
+ createUser({ followingCount: 99 }),
+ createUser({ followingCount: 100 }),
+ createUser({ followingCount: 101 }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'followingMoreThanOrEq',
+ value: 100,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ expect(actual3.some(r => r.id === role.id)).toBe(true);
+ });
+
+ test('ノート数が指定値以下', async () => {
+ const [user1, user2, user3] = await Promise.all([
+ createUser({ notesCount: 9 }),
+ createUser({ notesCount: 10 }),
+ createUser({ notesCount: 11 }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'notesLessThanOrEq',
+ value: 10,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(true);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ expect(actual3.some(r => r.id === role.id)).toBe(false);
+ });
+
+ test('ノート数が指定値以上', async () => {
+ const [user1, user2, user3] = await Promise.all([
+ createUser({ notesCount: 9 }),
+ createUser({ notesCount: 10 }),
+ createUser({ notesCount: 11 }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'notesMoreThanOrEq',
+ value: 10,
+ });
+
+ const actual1 = await roleService.getUserRoles(user1.id);
+ const actual2 = await roleService.getUserRoles(user2.id);
+ const actual3 = await roleService.getUserRoles(user3.id);
+ expect(actual1.some(r => r.id === role.id)).toBe(false);
+ expect(actual2.some(r => r.id === role.id)).toBe(true);
+ expect(actual3.some(r => r.id === role.id)).toBe(true);
});
});
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index b4b06b06bd..6962608106 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -13,11 +13,13 @@ import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
+import { CONTEXT } from '@/core/activitypub/misc/contexts.js';
import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js';
-import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js';
+import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
import { MiMeta, MiNote } from '@/models/_.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DownloadService } from '@/core/DownloadService.js';
@@ -88,6 +90,7 @@ describe('ActivityPub', () => {
let noteService: ApNoteService;
let personService: ApPersonService;
let rendererService: ApRendererService;
+ let jsonLdService: JsonLdService;
let resolver: MockResolver;
const metaInitial = {
@@ -128,6 +131,7 @@ describe('ActivityPub', () => {
personService = app.get<ApPersonService>(ApPersonService);
rendererService = app.get<ApRendererService>(ApRendererService);
imageService = app.get<ApImageService>(ApImageService);
+ jsonLdService = app.get<JsonLdService>(JsonLdService);
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
@@ -295,7 +299,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
imageObject,
);
- assert.ok(!driveFile.isLink);
+ assert.ok(driveFile && !driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
@@ -308,7 +312,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
- assert.ok(!sensitiveDriveFile.isLink);
+ assert.ok(sensitiveDriveFile && !sensitiveDriveFile.isLink);
});
test('cacheRemoteFiles=false disables caching', async () => {
@@ -324,7 +328,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
imageObject,
);
- assert.ok(driveFile.isLink);
+ assert.ok(driveFile && driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
@@ -337,7 +341,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
- assert.ok(sensitiveDriveFile.isLink);
+ assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink);
});
test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => {
@@ -353,7 +357,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
imageObject,
);
- assert.ok(!driveFile.isLink);
+ assert.ok(driveFile && !driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
@@ -366,7 +370,57 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
- assert.ok(sensitiveDriveFile.isLink);
+ assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink);
+ });
+
+ test('Link is not an attachment files', async () => {
+ const linkObject: IObject = {
+ type: 'Link',
+ href: 'https://example.com/',
+ };
+ const driveFile = await imageService.createImage(
+ await createRandomRemoteUser(resolver, personService),
+ linkObject,
+ );
+ assert.strictEqual(driveFile, null);
+ });
+ });
+
+ describe('JSON-LD', () =>{
+ test('Compaction', async () => {
+ const jsonLd = jsonLdService.use();
+
+ const object = {
+ '@context': [
+ 'https://www.w3.org/ns/activitystreams',
+ {
+ _misskey_quote: 'https://misskey-hub.net/ns#_misskey_quote',
+ unknown: 'https://example.org/ns#unknown',
+ undefined: null,
+ },
+ ],
+ id: 'https://example.com/notes/42',
+ type: 'Note',
+ attributedTo: 'https://example.com/users/1',
+ to: ['https://www.w3.org/ns/activitystreams#Public'],
+ content: 'test test foo',
+ _misskey_quote: 'https://example.com/notes/1',
+ unknown: 'test test bar',
+ undefined: 'test test baz',
+ };
+ const compacted = await jsonLd.compact(object);
+
+ assert.deepStrictEqual(compacted, {
+ '@context': CONTEXT,
+ id: 'https://example.com/notes/42',
+ type: 'Note',
+ attributedTo: 'https://example.com/users/1',
+ to: 'as:Public',
+ content: 'test test foo',
+ _misskey_quote: 'https://example.com/notes/1',
+ 'https://example.org/ns#unknown': 'test test bar',
+ // undefined: 'test test baz',
+ });
});
});
});
diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts
new file mode 100644
index 0000000000..ee16d421c4
--- /dev/null
+++ b/packages/backend/test/unit/entities/UserEntityService.ts
@@ -0,0 +1,528 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { CoreModule } from '@/core/CoreModule.js';
+import type { MiUser } from '@/models/User.js';
+import { secureRndstr } from '@/misc/secure-rndstr.js';
+import { genAidx } from '@/misc/id/aidx.js';
+import {
+ BlockingsRepository,
+ FollowingsRepository, FollowRequestsRepository,
+ MiUserProfile, MutingsRepository, RenoteMutingsRepository,
+ UserMemoRepository,
+ UserProfilesRepository,
+ UsersRepository,
+} from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { PageEntityService } from '@/core/entities/PageEntityService.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
+import { IdService } from '@/core/IdService.js';
+import { UtilityService } from '@/core/UtilityService.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
+import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
+import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
+import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
+import { MfmService } from '@/core/MfmService.js';
+import { HashtagService } from '@/core/HashtagService.js';
+import UsersChart from '@/core/chart/charts/users.js';
+import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js';
+import InstanceChart from '@/core/chart/charts/instance.js';
+import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
+import { ReactionService } from '@/core/ReactionService.js';
+import { NotificationService } from '@/core/NotificationService.js';
+
+process.env.NODE_ENV = 'test';
+
+describe('UserEntityService', () => {
+ describe('pack/packMany', () => {
+ let app: TestingModule;
+ let service: UserEntityService;
+ let usersRepository: UsersRepository;
+ let userProfileRepository: UserProfilesRepository;
+ let userMemosRepository: UserMemoRepository;
+ let followingRepository: FollowingsRepository;
+ let followingRequestRepository: FollowRequestsRepository;
+ let blockingRepository: BlockingsRepository;
+ let mutingRepository: MutingsRepository;
+ let renoteMutingsRepository: RenoteMutingsRepository;
+
+ async function createUser(userData: Partial<MiUser> = {}, profileData: Partial<MiUserProfile> = {}) {
+ const un = secureRndstr(16);
+ const user = await usersRepository
+ .insert({
+ ...userData,
+ id: genAidx(Date.now()),
+ username: un,
+ usernameLower: un,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfileRepository.insert({
+ ...profileData,
+ userId: user.id,
+ });
+
+ return user;
+ }
+
+ async function memo(writer: MiUser, target: MiUser, memo: string) {
+ await userMemosRepository.insert({
+ id: genAidx(Date.now()),
+ userId: writer.id,
+ targetUserId: target.id,
+ memo,
+ });
+ }
+
+ async function follow(follower: MiUser, followee: MiUser) {
+ await followingRepository.insert({
+ id: genAidx(Date.now()),
+ followerId: follower.id,
+ followeeId: followee.id,
+ });
+ }
+
+ async function requestFollow(requester: MiUser, requestee: MiUser) {
+ await followingRequestRepository.insert({
+ id: genAidx(Date.now()),
+ followerId: requester.id,
+ followeeId: requestee.id,
+ });
+ }
+
+ async function block(blocker: MiUser, blockee: MiUser) {
+ await blockingRepository.insert({
+ id: genAidx(Date.now()),
+ blockerId: blocker.id,
+ blockeeId: blockee.id,
+ });
+ }
+
+ async function mute(mutant: MiUser, mutee: MiUser) {
+ await mutingRepository.insert({
+ id: genAidx(Date.now()),
+ muterId: mutant.id,
+ muteeId: mutee.id,
+ });
+ }
+
+ async function muteRenote(mutant: MiUser, mutee: MiUser) {
+ await renoteMutingsRepository.insert({
+ id: genAidx(Date.now()),
+ muterId: mutant.id,
+ muteeId: mutee.id,
+ });
+ }
+
+ function randomIntRange(weight = 10) {
+ return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx);
+ }
+
+ beforeAll(async () => {
+ const services = [
+ UserEntityService,
+ ApPersonService,
+ NoteEntityService,
+ PageEntityService,
+ CustomEmojiService,
+ AnnouncementService,
+ RoleService,
+ FederatedInstanceService,
+ IdService,
+ AvatarDecorationService,
+ UtilityService,
+ EmojiEntityService,
+ ModerationLogService,
+ GlobalEventService,
+ DriveFileEntityService,
+ MetaService,
+ FetchInstanceMetadataService,
+ CacheService,
+ ApResolverService,
+ ApNoteService,
+ ApImageService,
+ ApMfmService,
+ MfmService,
+ HashtagService,
+ UsersChart,
+ ChartLoggerService,
+ InstanceChart,
+ ApLoggerService,
+ AccountMoveService,
+ ReactionService,
+ NotificationService,
+ ];
+
+ app = await Test.createTestingModule({
+ imports: [GlobalModule, CoreModule],
+ providers: [
+ ...services,
+ ...services.map(x => ({ provide: x.name, useExisting: x })),
+ ],
+ }).compile();
+ await app.init();
+ app.enableShutdownHooks();
+
+ service = app.get<UserEntityService>(UserEntityService);
+ usersRepository = app.get<UsersRepository>(DI.usersRepository);
+ userProfileRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
+ userMemosRepository = app.get<UserMemoRepository>(DI.userMemosRepository);
+ followingRepository = app.get<FollowingsRepository>(DI.followingsRepository);
+ followingRequestRepository = app.get<FollowRequestsRepository>(DI.followRequestsRepository);
+ blockingRepository = app.get<BlockingsRepository>(DI.blockingsRepository);
+ mutingRepository = app.get<MutingsRepository>(DI.mutingsRepository);
+ renoteMutingsRepository = app.get<RenoteMutingsRepository>(DI.renoteMutingsRepository);
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ test('UserLite', async() => {
+ const me = await createUser();
+ const who = await createUser();
+
+ await memo(me, who, 'memo');
+
+ const actual = await service.pack(who, me, { schema: 'UserLite' }) as any;
+ // no detail
+ expect(actual.memo).toBeUndefined();
+ // no detail and me
+ expect(actual.birthday).toBeUndefined();
+ // no detail and me
+ expect(actual.achievements).toBeUndefined();
+ });
+
+ test('UserDetailedNotMe', async() => {
+ const me = await createUser();
+ const who = await createUser({}, { birthday: '2000-01-01' });
+
+ await memo(me, who, 'memo');
+
+ const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any;
+ // is detail
+ expect(actual.memo).toBe('memo');
+ // is detail
+ expect(actual.birthday).toBe('2000-01-01');
+ // no detail and me
+ expect(actual.achievements).toBeUndefined();
+ });
+
+ test('MeDetailed', async() => {
+ const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+ const me = await createUser({}, {
+ birthday: '2000-01-01',
+ achievements: achievements,
+ });
+ await memo(me, me, 'memo');
+
+ const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any;
+ // is detail
+ expect(actual.memo).toBe('memo');
+ // is detail
+ expect(actual.birthday).toBe('2000-01-01');
+ // is detail and me
+ expect(actual.achievements).toEqual(achievements);
+ });
+
+ describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => {
+ test('no-preload', async() => {
+ const me = await createUser();
+ // meがフォローしてる人たち
+ const followeeMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of followeeMe) {
+ await follow(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(true);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meをフォローしてる人たち
+ const followerMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of followerMe) {
+ await follow(who, me);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(true);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meがフォローリクエストを送った人たち
+ const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of requestsFromYou) {
+ await requestFollow(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(true);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meにフォローリクエストを送った人たち
+ const requestsToYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of requestsToYou) {
+ await requestFollow(who, me);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(true);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meがブロックしてる人たち
+ const blockingYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of blockingYou) {
+ await block(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(true);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meをブロックしてる人たち
+ const blockingMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of blockingMe) {
+ await block(who, me);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(true);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meがミュートしてる人たち
+ const muters = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of muters) {
+ await mute(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(true);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+
+ // meがリノートミュートしてる人たち
+ const renoteMuters = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of renoteMuters) {
+ await muteRenote(me, who);
+ const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(true);
+ }
+ });
+
+ test('preload', async() => {
+ const me = await createUser();
+
+ {
+ // meがフォローしてる人たち
+ const followeeMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of followeeMe) {
+ await follow(me, who);
+ }
+ const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(true);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meをフォローしてる人たち
+ const followerMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of followerMe) {
+ await follow(who, me);
+ }
+ const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(true);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meがフォローリクエストを送った人たち
+ const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of requestsFromYou) {
+ await requestFollow(me, who);
+ }
+ const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(true);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meにフォローリクエストを送った人たち
+ const requestsToYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of requestsToYou) {
+ await requestFollow(who, me);
+ }
+ const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(true);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meがブロックしてる人たち
+ const blockingYou = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of blockingYou) {
+ await block(me, who);
+ }
+ const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(true);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meをブロックしてる人たち
+ const blockingMe = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of blockingMe) {
+ await block(who, me);
+ }
+ const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(true);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meがミュートしてる人たち
+ const muters = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of muters) {
+ await mute(me, who);
+ }
+ const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(true);
+ expect(actual.isRenoteMuted).toBe(false);
+ }
+ }
+
+ {
+ // meがリノートミュートしてる人たち
+ const renoteMuters = await Promise.all(randomIntRange().map(() => createUser()));
+ for (const who of renoteMuters) {
+ await muteRenote(me, who);
+ }
+ const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any;
+ for (const actual of actualList) {
+ expect(actual.isFollowing).toBe(false);
+ expect(actual.isFollowed).toBe(false);
+ expect(actual.hasPendingFollowRequestFromYou).toBe(false);
+ expect(actual.hasPendingFollowRequestToYou).toBe(false);
+ expect(actual.isBlocking).toBe(false);
+ expect(actual.isBlocked).toBe(false);
+ expect(actual.isMuted).toBe(false);
+ expect(actual.isRenoteMuted).toBe(true);
+ }
+ }
+ });
+ });
+ });
+});
diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts
new file mode 100644
index 0000000000..0b713e8bf6
--- /dev/null
+++ b/packages/backend/test/unit/misc/is-renote.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { isQuote, isRenote } from '@/misc/is-renote.js';
+import { MiNote } from '@/models/Note.js';
+
+const base: MiNote = {
+ id: 'some-note-id',
+ replyId: null,
+ reply: null,
+ renoteId: null,
+ renote: null,
+ threadId: null,
+ text: null,
+ name: null,
+ cw: null,
+ userId: 'some-user-id',
+ user: null,
+ localOnly: false,
+ reactionAcceptance: null,
+ renoteCount: 0,
+ repliesCount: 0,
+ clippedCount: 0,
+ reactions: {},
+ visibility: 'public',
+ uri: null,
+ url: null,
+ fileIds: [],
+ attachedFileTypes: [],
+ visibleUserIds: [],
+ mentions: [],
+ mentionedRemoteUsers: '',
+ reactionAndUserPairCache: [],
+ emojis: [],
+ tags: [],
+ hasPoll: false,
+ channelId: null,
+ channel: null,
+ userHost: null,
+ replyUserId: null,
+ replyUserHost: null,
+ renoteUserId: null,
+ renoteUserHost: null,
+};
+
+describe('misc:is-renote', () => {
+ test('note without renoteId should not be Renote', () => {
+ expect(isRenote(base)).toBe(false);
+ });
+
+ test('note with renoteId should be Renote and not be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id' };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(false);
+ });
+
+ test('note with renoteId and text should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', text: 'some-text' };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+
+ test('note with renoteId and cw should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', cw: 'some-cw' };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+
+ test('note with renoteId and replyId should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', replyId: 'some-reply-id' };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+
+ test('note with renoteId and poll should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', hasPoll: true };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+
+ test('note with renoteId and non-empty fileIds should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', fileIds: ['some-file-id'] };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+});
diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts
index fa37950951..2cf54e1555 100644
--- a/packages/backend/test/unit/misc/loader.ts
+++ b/packages/backend/test/unit/misc/loader.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { DebounceLoader } from '@/misc/loader.js';
class Mock {
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index cd5dddd68d..86814fffe0 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -9,11 +9,10 @@ import { basename, isAbsolute } from 'node:path';
import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws';
-import fetch, { File, RequestInit } from 'node-fetch';
+import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
-import { Packed } from '@/misc/json-schema.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
@@ -21,7 +20,7 @@ import type * as misskey from 'misskey-js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
-interface UserToken {
+export interface UserToken {
token: string;
bearer?: boolean;
}
@@ -35,20 +34,15 @@ export const cookie = (me: UserToken): string => {
return `token=${me.token};`;
};
-export const api = async (endpoint: string, params: any, me?: UserToken) => {
- const normalized = endpoint.replace(/^\//, '');
- return await request(`api/${normalized}`, params, me);
-};
-
-export type ApiRequest = {
- endpoint: string,
- parameters: object,
+export type ApiRequest<E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req'] = misskey.Endpoints[E]['req']> = {
+ endpoint: E,
+ parameters: P,
user: UserToken | undefined,
};
-export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
+export const successfulApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status?: number,
-} = {}): Promise<T> => {
+} = {}): Promise<misskey.api.SwitchCaseResponseType<E, P>> => {
const { endpoint, parameters, user } = request;
const res = await api(endpoint, parameters, user);
const status = assertion.status ?? (res.body == null ? 204 : 200);
@@ -56,7 +50,7 @@ export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body;
};
-export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
+export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status: number,
code: string,
id: string
@@ -70,7 +64,7 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body;
};
-const request = async (path: string, params: any, me?: UserToken): Promise<{
+export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{
status: number,
headers: Headers,
body: any
@@ -86,7 +80,7 @@ const request = async (path: string, params: any, me?: UserToken): Promise<{
bodyAuth.i = me.token;
}
- const res = await relativeFetch(path, {
+ const res = await relativeFetch(`api/${path}`, {
method: 'POST',
headers,
body: JSON.stringify(Object.assign(bodyAuth, params)),
@@ -141,7 +135,7 @@ export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']
return res.body;
};
-export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
+export const post = async (user: UserToken, params: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
const q = params;
const res = await api('notes/create', q, user);
@@ -159,8 +153,8 @@ export const createAppToken = async (user: UserToken, permissions: (typeof missk
};
// 非公開ノートをAPI越しに見たときのノート NoteEntityService.ts
-export const hiddenNote = (note: any): any => {
- const temp = {
+export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note => {
+ const temp: misskey.entities.Note = {
...note,
fileIds: [],
files: [],
@@ -173,21 +167,22 @@ export const hiddenNote = (note: any): any => {
return temp;
};
-export const react = async (user: UserToken, note: any, reaction: string): Promise<any> => {
+export const react = async (user: UserToken, note: misskey.entities.Note, reaction: string): Promise<void> => {
await api('notes/reactions/create', {
noteId: note.id,
reaction: reaction,
}, user);
};
-export const userList = async (user: UserToken, userList: any = {}): Promise<any> => {
+export const userList = async (user: UserToken, userList: Partial<misskey.entities.UserList> = {}): Promise<misskey.entities.UserList> => {
const res = await api('users/lists/create', {
name: 'test',
+ ...userList,
}, user);
return res.body;
};
-export const page = async (user: UserToken, page: any = {}): Promise<any> => {
+export const page = async (user: UserToken, page: Partial<misskey.entities.Page> = {}): Promise<misskey.entities.Page> => {
const res = await api('pages/create', {
alignCenter: false,
content: [
@@ -198,7 +193,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
},
],
eyeCatchingImageId: null,
- font: 'sans-serif',
+ font: 'sans-serif' as any,
hideTitleWhenPinned: false,
name: '1678594845072',
script: '',
@@ -210,7 +205,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
return res.body;
};
-export const play = async (user: UserToken, play: any = {}): Promise<any> => {
+export const play = async (user: UserToken, play: Partial<misskey.entities.Flash> = {}): Promise<misskey.entities.Flash> => {
const res = await api('flash/create', {
permissions: [],
script: 'test',
@@ -221,7 +216,7 @@ export const play = async (user: UserToken, play: any = {}): Promise<any> => {
return res.body;
};
-export const clip = async (user: UserToken, clip: any = {}): Promise<any> => {
+export const clip = async (user: UserToken, clip: Partial<misskey.entities.Clip> = {}): Promise<misskey.entities.Clip> => {
const res = await api('clips/create', {
description: null,
isPublic: true,
@@ -231,18 +226,18 @@ export const clip = async (user: UserToken, clip: any = {}): Promise<any> => {
return res.body;
};
-export const galleryPost = async (user: UserToken, channel: any = {}): Promise<any> => {
+export const galleryPost = async (user: UserToken, galleryPost: Partial<misskey.entities.GalleryPost> = {}): Promise<misskey.entities.GalleryPost> => {
const res = await api('gallery/posts/create', {
description: null,
fileIds: [],
isSensitive: false,
title: 'test',
- ...channel,
+ ...galleryPost,
}, user);
return res.body;
};
-export const channel = async (user: UserToken, channel: any = {}): Promise<any> => {
+export const channel = async (user: UserToken, channel: Partial<misskey.entities.Channel> = {}): Promise<misskey.entities.Channel> => {
const res = await api('channels/create', {
bannerId: null,
description: null,
@@ -252,7 +247,7 @@ export const channel = async (user: UserToken, channel: any = {}): Promise<any>
return res.body;
};
-export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise<any> => {
+export const role = async (user: UserToken, role: Partial<misskey.entities.Role> = {}, policies: any = {}): Promise<misskey.entities.Role> => {
const res = await api('admin/roles/create', {
asBadge: false,
canEditMembersByModerator: false,
@@ -260,7 +255,7 @@ export const role = async (user: UserToken, role: any = {}, policies: any = {}):
condFormula: {
id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85',
type: 'isRemote',
- },
+ } as any,
description: '',
displayOrder: 0,
iconUrl: null,
@@ -298,7 +293,7 @@ interface UploadOptions {
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{
status: number,
headers: Headers,
- body: misskey.Endpoints['drive/files/create']['res'] | null
+ body: misskey.entities.DriveFile | null
}> => {
const absPath = path == null
? new URL('resources/Lenna.jpg', import.meta.url)
@@ -335,14 +330,14 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
};
};
-export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'DriveFile'>> => {
+export const uploadUrl = async (user: UserToken, url: string): Promise<misskey.entities.DriveFile> => {
const marker = Math.random().toString();
const catcher = makeStreamCatcher(
user,
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
- (msg) => msg.body.file as Packed<'DriveFile'>,
+ (msg) => msg.body.file,
60 * 1000,
);