From 3b89b73d270f8c24600523092c7f23ccdf0b7f93 Mon Sep 17 00:00:00 2001 From: Luna Nova Date: Fri, 18 Oct 2024 23:20:47 -0400 Subject: [PATCH 001/154] fix: move `cypress` to `optionalDependencies` --- package.json | 4 +++- pnpm-lock.yaml | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a5d6771db6..1cd5f7e167 100644 --- a/package.json +++ b/package.json @@ -66,13 +66,15 @@ "esbuild": "0.23.1", "glob": "11.0.0" }, + "optionalDependencies": { + "cypress": "13.14.2" + }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.0.3", "@types/node": "20.14.12", "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "cross-env": "7.0.3", - "cypress": "13.14.2", "eslint": "9.8.0", "globals": "15.9.0", "ncp": "2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bda23dfd32..dd82433122 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,10 @@ importers: typescript: specifier: 5.6.2 version: 5.6.2 + optionalDependencies: + cypress: + specifier: 13.14.2 + version: 13.14.2 devDependencies: '@misskey-dev/eslint-plugin': specifier: 2.0.3 @@ -61,9 +65,6 @@ importers: cross-env: specifier: 7.0.3 version: 7.0.3 - cypress: - specifier: 13.14.2 - version: 13.14.2 eslint: specifier: 9.8.0 version: 9.8.0 @@ -17573,6 +17574,7 @@ snapshots: tmp: 0.2.3 untildify: 4.0.0 yauzl: 2.10.0 + optional: true cypress@13.15.0: dependencies: From 5b64b9001d746882b541ceb719142a34f06ec7eb Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 13 Oct 2024 21:07:15 -0400 Subject: [PATCH 002/154] fix weird spacing on notes/home.vue --- packages/frontend/src/pages/user/home.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 279f301d78..f6d2aec36f 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -141,8 +141,8 @@ SPDX-License-Identifier: AGPL-3.0-only - - + + + + + + + + + + diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index f6d2aec36f..4a7f626133 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -145,36 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - -
-
- -
{{ i18n.ts.noNotes }}
-
-
- -
-
- -
-
+
@@ -190,8 +161,6 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/pages/user/notes-container.vue b/packages/frontend/src/pages/user/notes-container.vue new file mode 100644 index 0000000000..da66f92020 --- /dev/null +++ b/packages/frontend/src/pages/user/notes-container.vue @@ -0,0 +1,109 @@ + + + + + + + From f5652605ecf29e1fa58b76640cf53f11d8f0f418 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 26 Oct 2024 11:00:45 -0400 Subject: [PATCH 004/154] remove notes-container.vue and revert refactor --- idea/Sharkey/SkPagination.vue | 88 +++++++++++++++++++ .../src/components => idea/Sharkey}/SkTab.vue | 0 .../user => idea/Sharkey}/notes-container.vue | 0 packages/frontend/src/pages/user/home.vue | 62 ++++++++++++- .../src/pages/user/index.timeline.vue | 70 ++++++++++++++- 5 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 idea/Sharkey/SkPagination.vue rename {packages/frontend/src/components => idea/Sharkey}/SkTab.vue (100%) rename {packages/frontend/src/pages/user => idea/Sharkey}/notes-container.vue (100%) diff --git a/idea/Sharkey/SkPagination.vue b/idea/Sharkey/SkPagination.vue new file mode 100644 index 0000000000..8a3fd03c28 --- /dev/null +++ b/idea/Sharkey/SkPagination.vue @@ -0,0 +1,88 @@ + + + + + + + + + diff --git a/packages/frontend/src/components/SkTab.vue b/idea/Sharkey/SkTab.vue similarity index 100% rename from packages/frontend/src/components/SkTab.vue rename to idea/Sharkey/SkTab.vue diff --git a/packages/frontend/src/pages/user/notes-container.vue b/idea/Sharkey/notes-container.vue similarity index 100% rename from packages/frontend/src/pages/user/notes-container.vue rename to idea/Sharkey/notes-container.vue diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 4a7f626133..398be8e9ab 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -145,7 +145,36 @@ SPDX-License-Identifier: AGPL-3.0-only - + + + + +
+
+ +
{{ i18n.ts.noNotes }}
+
+
+ +
+
+ +
+
@@ -161,6 +190,8 @@ SPDX-License-Identifier: AGPL-3.0-only + + From 1c181df0865d0695c081620b8af20b6e2af5be62 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 11:50:22 -0400 Subject: [PATCH 005/154] restore ordering of MkNotes attributes in index.timeline.vue --- packages/frontend/src/pages/user/index.timeline.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index 85e297e153..28a5091059 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + From a15e5c52f4d89f18a52fb0b9a81e1cc5e5ba0616 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 17:38:12 -0400 Subject: [PATCH 006/154] remove `idea` section --- idea/Sharkey/SkPagination.vue | 88 ------------------------- idea/Sharkey/SkTab.vue | 84 ------------------------ idea/Sharkey/notes-container.vue | 109 ------------------------------- 3 files changed, 281 deletions(-) delete mode 100644 idea/Sharkey/SkPagination.vue delete mode 100644 idea/Sharkey/SkTab.vue delete mode 100644 idea/Sharkey/notes-container.vue diff --git a/idea/Sharkey/SkPagination.vue b/idea/Sharkey/SkPagination.vue deleted file mode 100644 index 8a3fd03c28..0000000000 --- a/idea/Sharkey/SkPagination.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - diff --git a/idea/Sharkey/SkTab.vue b/idea/Sharkey/SkTab.vue deleted file mode 100644 index d5266e9196..0000000000 --- a/idea/Sharkey/SkTab.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - diff --git a/idea/Sharkey/notes-container.vue b/idea/Sharkey/notes-container.vue deleted file mode 100644 index da66f92020..0000000000 --- a/idea/Sharkey/notes-container.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - From 8477909af208b7e0f3ce9357350be6b0a0fc783d Mon Sep 17 00:00:00 2001 From: Kio! Date: Sun, 3 Nov 2024 19:50:25 +0000 Subject: [PATCH 007/154] Update report-abuse.ts --- .../backend/src/server/api/endpoints/users/report-abuse.ts | 4 ---- 1 file changed, 4 deletions(-) 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 5ff6de37d2..38ded8ee1e 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -66,10 +66,6 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.cannotReportYourself); } - if (await this.roleService.isAdministrator(targetUser)) { - throw new ApiError(meta.errors.cannotReportAdmin); - } - await this.abuseReportService.report([{ targetUserId: targetUser.id, targetUserHost: targetUser.host, From c2c2120b768adbc8f710f65f7a0fe9658efe6683 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Mon, 4 Nov 2024 22:50:56 +0100 Subject: [PATCH 008/154] Center SkModPlayer on big displays Authored-by: Freeplay Co-authored-by: Freeplay --- packages/frontend/src/components/SkModPlayer.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/components/SkModPlayer.vue b/packages/frontend/src/components/SkModPlayer.vue index 58a96cfb63..6ba983645e 100644 --- a/packages/frontend/src/components/SkModPlayer.vue +++ b/packages/frontend/src/components/SkModPlayer.vue @@ -451,6 +451,7 @@ onDeactivated(() => { overflow: hidden; display: flex; flex-direction: column; + justify-content: center; > i { display: block; From 0f07f27642344eb893bcb0ccf6f29ba2c1da677c Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Tue, 5 Nov 2024 01:10:49 -0500 Subject: [PATCH 009/154] chore: Bump version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b312044921..6e84882440 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.9.1", + "version": "2024.10.0-dev", "codename": "shonk", "repository": { "type": "git", From 9fe5dc679aba9b0bb3b4d743ddd0ead341ccb8d6 Mon Sep 17 00:00:00 2001 From: dakkar Date: Tue, 5 Nov 2024 14:21:58 +0000 Subject: [PATCH 010/154] check harder for connectibility `allSettled` does not throw if a promise is rejected, so `check_connect` never actually failed --- packages/backend/scripts/check_connect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js index f33a450325..17b198ef62 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/scripts/check_connect.js @@ -57,4 +57,4 @@ const promises = Array connectToPostgres() ]); -await Promise.allSettled(promises); +await Promise.all(promises); From e0a2e7aedc84f8a6f2b56f20edbe72ba800d48b1 Mon Sep 17 00:00:00 2001 From: piuvas Date: Tue, 5 Nov 2024 20:22:56 -0300 Subject: [PATCH 011/154] animations following feed --- packages/frontend/src/pages/following-feed.vue | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index d45f572739..d4bc295c78 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -257,6 +257,21 @@ definePageMetadata(() => ({ margin-bottom: 12px; } +@keyframes border { + from {border-left: 0px solid var(--accent);} + to {border-left: 6px solid var(--accent);} +} + +.selected { + animation: border 0.2s ease-out 0s 1 forwards; + &:first-child { + border-top-left-radius: 5px; + } + &:last-child { + border-bottom-left-radius: 5px; + } +} + @media (min-width: 750px) { .root { grid-template-columns: min-content 4fr 6fr min-content; From 7f9a15105571748ca0988eaf38d4f9f413f94bce Mon Sep 17 00:00:00 2001 From: piuvas Date: Tue, 5 Nov 2024 20:27:24 -0300 Subject: [PATCH 012/154] give ff entries clickable pointer --- packages/frontend/src/components/SkFollowingFeedEntry.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/components/SkFollowingFeedEntry.vue b/packages/frontend/src/components/SkFollowingFeedEntry.vue index 8fa5e014d8..75539f1de7 100644 --- a/packages/frontend/src/components/SkFollowingFeedEntry.vue +++ b/packages/frontend/src/components/SkFollowingFeedEntry.vue @@ -54,6 +54,7 @@ defineEmits<{ overflow-wrap: break-word; display: flex; contain: content; + cursor: pointer; } .avatar { From 83f780978c563781981d71455e346eae9d28ecaf Mon Sep 17 00:00:00 2001 From: Maciej Date: Thu, 7 Nov 2024 07:57:35 +0000 Subject: [PATCH 013/154] Change example config - db name and user consistent with docs --- .config/example.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/example.yml b/.config/example.yml index 0062b6670c..cf7b972de5 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -99,10 +99,10 @@ db: port: 5432 # Database name - db: misskey + db: sharkey # Auth - user: example-misskey-user + user: sharkey pass: example-misskey-pass # Whether disable Caching queries From 00ab7f5bd1c954119118707e839fc549733c3169 Mon Sep 17 00:00:00 2001 From: Rachel Y Date: Thu, 7 Nov 2024 20:09:01 +0000 Subject: [PATCH 014/154] Update file Dockerfile --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index acef95deab..0e156ef003 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,7 @@ RUN apk add ffmpeg tini jemalloc \ && corepack enable \ && addgroup -g "${GID}" sharkey \ && adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \ + && mkdir /sharkey/files \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -exec chmod u-s {} \; \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -exec chmod g-s {} \; From aebdbf07b4b11c8bc69d77b967222ec4a0bb0141 Mon Sep 17 00:00:00 2001 From: Rachel Y Date: Thu, 7 Nov 2024 20:09:52 +0000 Subject: [PATCH 015/154] creat and chown /sharkey/files in dockerfile --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 0e156ef003..abee7fb098 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,6 +41,7 @@ RUN apk add ffmpeg tini jemalloc \ && addgroup -g "${GID}" sharkey \ && adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \ && mkdir /sharkey/files \ + && chown sharkey:sharkey /sharkey/files \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -exec chmod u-s {} \; \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -exec chmod g-s {} \; From 03559156b923ce5e337c998868f4fe12acfb7f14 Mon Sep 17 00:00:00 2001 From: Caramel Date: Sat, 9 Nov 2024 00:32:03 +0100 Subject: [PATCH 016/154] Improve performance of notes/following API --- .../server/api/endpoints/notes/following.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index b6604b9798..f8e9e5c4a1 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -103,6 +103,15 @@ export default class extends Endpoint { // eslint- sub.andWhere('latest.is_quote = false'); } + // Select the appropriate collection of users + if (ps.list === 'followers') { + addFollower(sub); + } else if (ps.list === 'following') { + addFollowee(sub); + } else { + addMutual(sub); + } + return sub; }, 'latest', @@ -118,15 +127,6 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.channel', 'channel') ; - // Select the appropriate collection of users - if (ps.list === 'followers') { - addFollower(query); - } else if (ps.list === 'following') { - addFollowee(query); - } else { - addMutual(query); - } - // Limit to files, if requested if (ps.filesOnly) { query.andWhere('note."fileIds" != \'{}\''); From 906c2863db0d6d65213c63e62e97eb77247ad017 Mon Sep 17 00:00:00 2001 From: Luna Nova Date: Tue, 12 Nov 2024 14:33:05 -0500 Subject: [PATCH 017/154] fix: move `cypress` to `optionalDependencies` in `packages/frontent/package.json` --- packages/frontend/package.json | 4 +- pnpm-lock.yaml | 129 ++++++++++++++++++++++++--------- 2 files changed, 99 insertions(+), 34 deletions(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 1b4b5d5bd1..6198e02f61 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -76,6 +76,9 @@ "vue": "3.5.10", "vuedraggable": "next" }, + "optionalDependencies": { + "cypress": "13.15.0" + }, "devDependencies": { "@misskey-dev/summaly": "5.1.0", "@storybook/addon-actions": "8.3.3", @@ -115,7 +118,6 @@ "@vue/runtime-core": "3.5.10", "acorn": "8.12.1", "cross-env": "7.0.3", - "cypress": "13.15.0", "eslint-plugin-import": "2.30.0", "eslint-plugin-vue": "9.28.0", "fast-glob": "3.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd82433122..d2d7035550 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -880,6 +880,10 @@ importers: vuedraggable: specifier: next version: 4.1.0(vue@3.5.10(typescript@5.6.2)) + optionalDependencies: + cypress: + specifier: 13.15.0 + version: 13.15.0 devDependencies: '@misskey-dev/summaly': specifier: 5.1.0 @@ -995,9 +999,6 @@ importers: cross-env: specifier: 7.0.3 version: 7.0.3 - cypress: - specifier: 13.15.0 - version: 13.15.0 eslint-plugin-import: specifier: 2.30.0 version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0) @@ -11362,6 +11363,9 @@ packages: vue-component-type-helpers@2.0.16: resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==} + vue-component-type-helpers@2.1.10: + resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==} + vue-component-type-helpers@2.1.6: resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==} @@ -12702,6 +12706,7 @@ snapshots: tough-cookie: 4.1.4 tunnel-agent: 0.6.0 uuid: 8.3.2 + optional: true '@cypress/xvfb@1.2.4(supports-color@8.1.1)': dependencies: @@ -12709,6 +12714,7 @@ snapshots: lodash.once: 4.1.1 transitivePeerDependencies: - supports-color + optional: true '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@4.0.0)': dependencies: @@ -15115,7 +15121,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.5.10(typescript@5.6.2) - vue-component-type-helpers: 2.1.6 + vue-component-type-helpers: 2.1.10 '@swc/cli@0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)': dependencies: @@ -15701,11 +15707,13 @@ snapshots: dependencies: '@types/sinonjs__fake-timers': 8.1.5 - '@types/sinonjs__fake-timers@8.1.1': {} + '@types/sinonjs__fake-timers@8.1.1': + optional: true '@types/sinonjs__fake-timers@8.1.5': {} - '@types/sizzle@2.3.3': {} + '@types/sizzle@2.3.3': + optional: true '@types/stack-utils@2.0.1': {} @@ -16638,7 +16646,8 @@ snapshots: dependencies: tslib: 2.6.3 - astral-regex@2.0.0: {} + astral-regex@2.0.0: + optional: true astring@1.9.0: {} @@ -16652,7 +16661,8 @@ snapshots: asynckit@0.4.0: {} - at-least-node@1.0.0: {} + at-least-node@1.0.0: + optional: true atomic-sleep@1.0.0: {} @@ -16673,9 +16683,11 @@ snapshots: sinon: 16.1.3 tslib: 2.6.2 - aws-sign2@0.7.0: {} + aws-sign2@0.7.0: + optional: true - aws4@1.12.0: {} + aws4@1.12.0: + optional: true axios@0.24.0: dependencies: @@ -16829,7 +16841,8 @@ snapshots: binary-extensions@2.2.0: {} - blob-util@2.0.2: {} + blob-util@2.0.2: + optional: true bluebird@3.7.2: {} @@ -16923,7 +16936,8 @@ snapshots: dependencies: node-int64: 0.4.0 - buffer-crc32@0.2.13: {} + buffer-crc32@0.2.13: + optional: true buffer-crc32@1.0.0: {} @@ -16940,6 +16954,7 @@ snapshots: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + optional: true buffer@6.0.3: dependencies: @@ -17027,7 +17042,8 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 - cachedir@2.3.0: {} + cachedir@2.3.0: + optional: true call-bind@1.0.2: dependencies: @@ -17071,7 +17087,8 @@ snapshots: canvas-confetti@1.9.3: {} - caseless@0.12.0: {} + caseless@0.12.0: + optional: true cbor@9.0.2: dependencies: @@ -17219,6 +17236,7 @@ snapshots: cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 + optional: true cli-highlight@2.1.11: dependencies: @@ -17236,11 +17254,13 @@ snapshots: string-width: 4.2.3 optionalDependencies: '@colors/colors': 1.5.0 + optional: true cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 string-width: 4.2.3 + optional: true cli-width@4.1.0: {} @@ -17298,7 +17318,8 @@ snapshots: colord@2.9.3: {} - colorette@2.0.19: {} + colorette@2.0.19: + optional: true combined-stream@1.0.8: dependencies: @@ -17310,7 +17331,8 @@ snapshots: commander@2.20.3: {} - commander@6.2.1: {} + commander@6.2.1: + optional: true commander@7.2.0: {} @@ -17318,7 +17340,8 @@ snapshots: commander@9.5.0: {} - common-tags@1.8.2: {} + common-tags@1.8.2: + optional: true commondir@1.0.1: {} @@ -17620,6 +17643,7 @@ snapshots: tmp: 0.2.3 untildify: 4.0.0 yauzl: 2.10.0 + optional: true dashdash@1.14.1: dependencies: @@ -17931,6 +17955,7 @@ snapshots: enquirer@2.3.6: dependencies: ansi-colors: 4.1.3 + optional: true entities@2.2.0: {} @@ -18486,7 +18511,8 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter2@6.4.7: {} + eventemitter2@6.4.7: + optional: true eventemitter3@4.0.7: {} @@ -18515,6 +18541,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 + optional: true execa@5.1.1: dependencies: @@ -18675,6 +18702,7 @@ snapshots: '@types/yauzl': 2.10.0 transitivePeerDependencies: - supports-color + optional: true extsprintf@1.3.0: {} @@ -18782,6 +18810,7 @@ snapshots: fd-slicer@1.1.0: dependencies: pend: 1.2.0 + optional: true feed@4.2.2: dependencies: @@ -18795,6 +18824,7 @@ snapshots: figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 + optional: true figures@6.1.0: dependencies: @@ -18935,7 +18965,8 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 - forever-agent@0.6.1: {} + forever-agent@0.6.1: + optional: true form-data-encoder@2.1.4: {} @@ -18981,6 +19012,7 @@ snapshots: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 + optional: true fs-minipass@2.1.0: dependencies: @@ -19069,6 +19101,7 @@ snapshots: getos@3.2.1: dependencies: async: 3.2.4 + optional: true getpass@0.1.7: dependencies: @@ -19126,6 +19159,7 @@ snapshots: global-dirs@3.0.1: dependencies: ini: 2.0.0 + optional: true globals@11.12.0: {} @@ -19350,6 +19384,7 @@ snapshots: assert-plus: 1.0.0 jsprim: 2.0.2 sshpk: 1.18.0 + optional: true http2-wrapper@1.0.3: dependencies: @@ -19377,7 +19412,8 @@ snapshots: transitivePeerDependencies: - supports-color - human-signals@1.1.1: {} + human-signals@1.1.1: + optional: true human-signals@2.1.0: {} @@ -19451,7 +19487,8 @@ snapshots: ini@1.3.8: {} - ini@2.0.0: {} + ini@2.0.0: + optional: true insert-text-at-cursor@0.3.0: {} @@ -19544,6 +19581,7 @@ snapshots: is-ci@3.0.1: dependencies: ci-info: 3.7.1 + optional: true is-core-module@2.13.1: dependencies: @@ -19588,6 +19626,7 @@ snapshots: dependencies: global-dirs: 3.0.1 is-path-inside: 3.0.3 + optional: true is-ip@3.1.0: dependencies: @@ -19668,7 +19707,8 @@ snapshots: dependencies: which-typed-array: 1.1.15 - is-typedarray@1.0.0: {} + is-typedarray@1.0.0: + optional: true is-unicode-supported@0.1.0: {} @@ -19699,7 +19739,8 @@ snapshots: isexe@3.1.1: {} - isstream@0.1.2: {} + isstream@0.1.2: + optional: true istanbul-lib-coverage@3.2.2: {} @@ -20298,6 +20339,7 @@ snapshots: extsprintf: 1.3.0 json-schema: 0.4.0 verror: 1.10.0 + optional: true jsrsasign@11.1.0: {} @@ -20384,6 +20426,7 @@ snapshots: wrap-ansi: 7.0.0 optionalDependencies: enquirer: 2.3.6 + optional: true local-pkg@0.5.0: dependencies: @@ -20408,7 +20451,8 @@ snapshots: lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} + lodash.once@4.1.1: + optional: true lodash.uniq@4.5.0: {} @@ -20425,6 +20469,7 @@ snapshots: cli-cursor: 3.1.0 slice-ansi: 4.0.0 wrap-ansi: 6.2.0 + optional: true longest-streak@3.1.0: {} @@ -21402,7 +21447,8 @@ snapshots: os-utils@0.0.14: {} - ospath@1.2.2: {} + ospath@1.2.2: + optional: true otpauth@9.3.2: dependencies: @@ -21553,9 +21599,11 @@ snapshots: peek-readable@5.2.0: {} - pend@1.2.0: {} + pend@1.2.0: + optional: true - performance-now@2.1.0: {} + performance-now@2.1.0: + optional: true pg-cloudflare@1.1.1: optional: true @@ -21874,7 +21922,8 @@ snapshots: prettier@3.3.3: {} - pretty-bytes@5.6.0: {} + pretty-bytes@5.6.0: + optional: true pretty-format@27.5.1: dependencies: @@ -21952,7 +22001,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.0.0: {} + proxy-from-env@1.0.0: + optional: true proxy-from-env@1.1.0: {} @@ -22310,6 +22360,7 @@ snapshots: request-progress@3.0.0: dependencies: throttleit: 1.0.0 + optional: true require-directory@2.1.1: {} @@ -22359,6 +22410,7 @@ snapshots: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 + optional: true ret@0.5.0: {} @@ -22366,7 +22418,8 @@ snapshots: reusify@1.0.4: {} - rfdc@1.3.0: {} + rfdc@1.3.0: + optional: true rfdc@1.4.1: {} @@ -22746,12 +22799,14 @@ snapshots: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + optional: true slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + optional: true slick@1.12.2: {} @@ -22851,6 +22906,7 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 tweetnacl: 0.14.5 + optional: true ssri@10.0.4: dependencies: @@ -23147,7 +23203,8 @@ snapshots: throttle-debounce@5.0.2: {} - throttleit@1.0.0: {} + throttleit@1.0.0: + optional: true through@2.3.8: {} @@ -23308,6 +23365,7 @@ snapshots: tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 + optional: true tweetnacl@0.14.5: {} @@ -23525,7 +23583,8 @@ snapshots: webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 - untildify@4.0.0: {} + untildify@4.0.0: + optional: true update-browserslist-db@1.0.13(browserslist@4.22.2): dependencies: @@ -23572,7 +23631,8 @@ snapshots: uuid@10.0.0: {} - uuid@8.3.2: {} + uuid@8.3.2: + optional: true uuid@9.0.1: {} @@ -23768,6 +23828,8 @@ snapshots: vue-component-type-helpers@2.0.16: {} + vue-component-type-helpers@2.1.10: {} + vue-component-type-helpers@2.1.6: {} vue-demi@0.14.7(vue@3.5.10(typescript@5.6.2)): @@ -24091,6 +24153,7 @@ snapshots: dependencies: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + optional: true yocto-queue@0.1.0: {} From 101ca9e0f7e746f2188eac93d597fc42087e3662 Mon Sep 17 00:00:00 2001 From: tess Date: Tue, 12 Nov 2024 21:16:59 +0100 Subject: [PATCH 018/154] make sure popup position is never off screen to the left --- packages/frontend/src/scripts/popup-position.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index 3dad41a8b3..80ad91c48f 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -15,6 +15,8 @@ export function calcPopupPosition(el: HTMLElement, props: { const contentWidth = el.offsetWidth; const contentHeight = el.offsetHeight; + const LEFT_MARGIN = 4; + let rect: DOMRect; if (props.anchorElement) { @@ -39,6 +41,8 @@ export function calcPopupPosition(el: HTMLElement, props: { left = window.innerWidth - contentWidth + window.scrollX - 1; } + left = Math.max(LEFT_MARGIN, left); + return [left, top]; }; @@ -60,6 +64,8 @@ export function calcPopupPosition(el: HTMLElement, props: { left = window.innerWidth - contentWidth + window.scrollX - 1; } + left = Math.max(LEFT_MARGIN, left); + return [left, top]; }; @@ -75,6 +81,8 @@ export function calcPopupPosition(el: HTMLElement, props: { top = props.y; } + left = Math.max(LEFT_MARGIN, left); + top -= (el.offsetHeight / 2); if (top + contentHeight - window.scrollY > window.innerHeight) { @@ -106,6 +114,8 @@ export function calcPopupPosition(el: HTMLElement, props: { top -= (el.offsetHeight / 2); } + left = Math.max(LEFT_MARGIN, left); + if (top + contentHeight - window.scrollY > window.innerHeight) { top = window.innerHeight - contentHeight + window.scrollY - 1; } From 19be113cb4f62eb479061fe5274fce0655ee232b Mon Sep 17 00:00:00 2001 From: tess Date: Tue, 12 Nov 2024 21:29:22 +0100 Subject: [PATCH 019/154] Keep MkUserPopup from extending past left side of screen --- packages/frontend/src/components/MkUserPopup.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index c6f4699b3e..e98a8b85e9 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -119,7 +119,7 @@ onMounted(() => { } const rect = props.source.getBoundingClientRect(); - const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.scrollX; + const x = Math.max(2, ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.scrollX); const y = rect.top + props.source.offsetHeight + window.scrollY; top.value = y; From 6d6b03dfe262c2477284aacd3da85a944b6d55b0 Mon Sep 17 00:00:00 2001 From: tess Date: Tue, 12 Nov 2024 21:30:19 +0100 Subject: [PATCH 020/154] tweak popup left margin for consistency --- packages/frontend/src/scripts/popup-position.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index 80ad91c48f..187896b0e5 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -15,7 +15,7 @@ export function calcPopupPosition(el: HTMLElement, props: { const contentWidth = el.offsetWidth; const contentHeight = el.offsetHeight; - const LEFT_MARGIN = 4; + const LEFT_MARGIN = 2; let rect: DOMRect; From 68e5b5a84a8ea088f7375eb1056218c4bcf2b05a Mon Sep 17 00:00:00 2001 From: tess Date: Tue, 12 Nov 2024 22:09:37 +0100 Subject: [PATCH 021/154] Set horizontal margin for even better consistency --- packages/frontend/src/components/MkUserPopup.vue | 2 +- packages/frontend/src/scripts/popup-position.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index e98a8b85e9..4ae23de62c 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -119,7 +119,7 @@ onMounted(() => { } const rect = props.source.getBoundingClientRect(); - const x = Math.max(2, ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.scrollX); + const x = Math.max(1, ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.scrollX); const y = rect.top + props.source.offsetHeight + window.scrollY; top.value = y; diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index 187896b0e5..be49532cf8 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -15,7 +15,7 @@ export function calcPopupPosition(el: HTMLElement, props: { const contentWidth = el.offsetWidth; const contentHeight = el.offsetHeight; - const LEFT_MARGIN = 2; + const HORIZONTAL_MARGIN = 1; let rect: DOMRect; @@ -38,10 +38,10 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - 1; + left = window.innerWidth - contentWidth + window.scrollX - HORIZONTAL_MARGIN; } - left = Math.max(LEFT_MARGIN, left); + left = Math.max(HORIZONTAL_MARGIN, left); return [left, top]; }; @@ -61,10 +61,10 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - 1; + left = window.innerWidth - contentWidth + window.scrollX - HORIZONTAL_MARGIN; } - left = Math.max(LEFT_MARGIN, left); + left = Math.max(HORIZONTAL_MARGIN, left); return [left, top]; }; @@ -81,7 +81,7 @@ export function calcPopupPosition(el: HTMLElement, props: { top = props.y; } - left = Math.max(LEFT_MARGIN, left); + left = Math.max(HORIZONTAL_MARGIN, left); top -= (el.offsetHeight / 2); @@ -114,7 +114,7 @@ export function calcPopupPosition(el: HTMLElement, props: { top -= (el.offsetHeight / 2); } - left = Math.max(LEFT_MARGIN, left); + left = Math.max(HORIZONTAL_MARGIN, left); if (top + contentHeight - window.scrollY > window.innerHeight) { top = window.innerHeight - contentHeight + window.scrollY - 1; From eaad96aae30d0e1ab1ae05b5c0fef908cd002d79 Mon Sep 17 00:00:00 2001 From: piuvas Date: Fri, 15 Nov 2024 13:40:53 -0300 Subject: [PATCH 022/154] edit query --- .../backend/src/server/api/endpoints/emojis.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 46ef4eca1b..8054de3d95 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -50,16 +50,11 @@ export default class extends Endpoint { // eslint- private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { - const emojis = await this.emojisRepository.find({ - where: { - host: IsNull(), - }, - order: { - category: 'ASC', - name: 'ASC', - }, - }); - + const emojis = await this.emojisRepository.createQueryBuilder() + .where('host IS NULL') + .orderBy('LOWER(category)', 'ASC') + .orderBy('LOWER(name)', 'ASC') + .getMany() return { emojis: await this.emojiEntityService.packSimpleMany(emojis), }; From 41c500851b62b343ee47392f39cacbee3d1b20b8 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 00:54:30 -0500 Subject: [PATCH 023/154] Bump develop version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ba3881756..6e84882440 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.9.2", + "version": "2024.10.0-dev", "codename": "shonk", "repository": { "type": "git", From fb54546573f267701bd82175dc26a431518fb6c8 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 01:17:24 -0500 Subject: [PATCH 024/154] Fix linter error in emojis endpoint --- packages/backend/src/server/api/endpoints/emojis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 8054de3d95..4dd3a2ed50 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -54,7 +54,7 @@ export default class extends Endpoint { // eslint- .where('host IS NULL') .orderBy('LOWER(category)', 'ASC') .orderBy('LOWER(name)', 'ASC') - .getMany() + .getMany(); return { emojis: await this.emojiEntityService.packSimpleMany(emojis), }; From d883934826122b64439170c2013d65606fd23184 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 05:13:35 +0200 Subject: [PATCH 025/154] fix: primitive 2: acceptance of cross-origin alternate links --- packages/backend/src/core/activitypub/ApRequestService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 38c78cf900..35eb1a5c53 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -18,6 +18,7 @@ import type Logger from '@/logger.js'; import type { IObject } from './type.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js'; +import { UtilityService } from "@/core/UtilityService.js"; type Request = { url: string; @@ -147,6 +148,7 @@ export class ApRequestService { private userKeypairService: UserKeypairService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, + private utilityService: UtilityService, ) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる @@ -241,7 +243,9 @@ export class ApRequestService { if (alternate) { const href = alternate.getAttribute('href'); if (href) { - return await this.signedGet(href, user, false); + if (this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) { + return await this.signedGet(href, user, false); + } } } } catch (e) { From 9090b745e69c380847f97f107d39864d7ace0039 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 04:04:56 +0200 Subject: [PATCH 026/154] fix: primitive 3: validation of non-final url --- packages/backend/src/core/HttpRequestService.ts | 2 +- packages/backend/src/core/activitypub/ApRequestService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index bea5dee6ab..08e9f46b2d 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -129,7 +129,7 @@ export class HttpRequestService { const finalUrl = res.url; // redirects may have been involved const activity = await res.json() as IObject; - assertActivityMatchesUrls(activity, [url, finalUrl]); + assertActivityMatchesUrls(activity, [finalUrl]); return activity; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 35eb1a5c53..eeff73385b 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -261,7 +261,7 @@ export class ApRequestService { const finalUrl = res.url; // redirects may have been involved const activity = await res.json() as IObject; - assertActivityMatchesUrls(activity, [url, finalUrl]); + assertActivityMatchesUrls(activity, [finalUrl]); return activity; } From 1e14612f0e4349b00f5016f3a9184ff3e40fc37e Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 04:11:35 +0200 Subject: [PATCH 027/154] fix: primitive 4: missing same-origin identifier validation of collection-wrapped activities --- packages/backend/src/core/activitypub/ApInboxService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index d54c9544c3..5a6f6f083f 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -100,6 +100,10 @@ export class ApInboxService { const resolver = this.apResolverService.createResolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { const act = await resolver.resolve(item); + if (act.id == null || this.utilityService.extractDbHost(act.id) !== this.utilityService.extractDbHost(actor.uri)) { + this.logger.debug('skipping activity: activity id is null or mismatching'); + continue; + } try { results.push([getApId(item), await this.performOneActivity(actor, act)]); } catch (err) { From ad8e8793c7b0ecc08bb271cd83ba04f6f8be7036 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 04:37:47 +0200 Subject: [PATCH 028/154] fix: primitives 5 & 8: reject activities with non-string identifiers --- packages/backend/src/queue/processors/InboxProcessorService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 11b00bb683..f453d7d1ae 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -193,6 +193,9 @@ export class InboxProcessorService implements OnApplicationShutdown { throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`); } } + else { + throw new Bull.UnrecoverableError('skip: activity id is not a string'); + } // Update stats this.federatedInstanceService.fetch(authUser.user.host).then(i => { From 174dfb83d09d13876c65b98c75769d01f5c0ec47 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 04:28:43 +0200 Subject: [PATCH 029/154] fix: primitive 6: reject anonymous objects that were fetched by their id --- packages/backend/src/core/activitypub/ApResolverService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 5d5c61ce2c..a2c7ed19d8 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -121,7 +121,11 @@ export class Resolver { // `object.id` or `object.url` matches the URL used to fetch the // object after redirects; here we double-check that no redirects // bounced between hosts - if (object.id && (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value))) { + if (object.id == null) { + throw new Error('invalid AP object: missing id'); + } + + if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) { throw new Error(`invalid AP object ${value}: id ${object.id} has different host`); } From 9ab25ede28f4f04ac2ae48c947e7668a9a6012b2 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 04:40:33 +0200 Subject: [PATCH 030/154] fix: primitives 9, 10 & 11: http signature validation doesn't enforce required headers or specify auth header name --- packages/backend/src/server/ActivityPubServerService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 52592c47c6..f955329fd1 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -152,7 +152,7 @@ export class ActivityPubServerService { let signature; try { - signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); + signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' }); } catch (e) { // not signed, or malformed signature: refuse this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`); @@ -229,7 +229,7 @@ export class ActivityPubServerService { let signature; try { - signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); + signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'digest', 'host', 'date'], authorizationHeaderName: 'signature' }); } catch (e) { reply.code(401); return; From 1c7e05ce9ee6a4a5669ec73c940dcf57ecdc3db5 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 19:57:29 -0500 Subject: [PATCH 031/154] fix: primitive 7 & 12: prevent poll spoofing --- .../src/core/activitypub/ApInboxService.ts | 2 +- .../activitypub/models/ApQuestionService.ts | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 5a6f6f083f..edd1041062 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -786,7 +786,7 @@ export class ApInboxService { await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; } else if (getApType(object) === 'Question') { - await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); + await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); return 'ok: Question updated'; } else if (getApType(object) === 'Note') { await this.apNoteService.updateNote(object, resolver).catch(err => console.error(err)); diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 9246398fde..c1aea15ece 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -5,16 +5,17 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, PollsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { IPoll } from '@/models/Poll.js'; +import type { MiRemoteUser } from '@/models/User.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { isQuestion } from '../type.js'; +import { getOneApId, isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; -import type { IObject, IQuestion } from '../type.js'; +import type { IObject } from '../type.js'; @Injectable() export class ApQuestionService { @@ -24,6 +25,9 @@ export class ApQuestionService { @Inject(DI.config) private config: Config, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -65,7 +69,7 @@ export class ApQuestionService { * @returns true if updated */ @bindThis - public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise { + public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise { const uri = typeof value === 'string' ? value : value.id; if (uri == null) throw new Error('uri is null'); @@ -78,15 +82,26 @@ export class ApQuestionService { const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); if (poll == null) throw new Error('Question is not registered'); + + const user = await this.usersRepository.findOneBy({ id: poll.userId }); + if (user == null) throw new Error('Question is not registered'); //#endregion // resolve new Question object // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); - const question = await resolver.resolve(value) as IQuestion; + const question = await resolver.resolve(value); this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - if (question.type !== 'Question') throw new Error('object is not a Question'); + if (!isQuestion(question)) throw new Error('object is not a Question'); + + const attribution = (question.attributedTo) ? getOneApId(question.attributedTo) : user.uri; + const attributionMatchesExisting = attribution === user.uri; + const actorMatchesAttribution = (actor) ? attribution === actor.uri : true; + + if (!attributionMatchesExisting || !actorMatchesAttribution) { + throw new Error('Refusing to ingest update for poll by different user'); + } const apChoices = question.oneOf ?? question.anyOf; if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices); @@ -96,7 +111,7 @@ export class ApQuestionService { for (const choice of poll.choices) { const oldCount = poll.votes[poll.choices.indexOf(choice)]; const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems; - if (newCount == null) throw new Error('invalid newCount: ' + newCount); + if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new Error('invalid newCount: ' + newCount); if (oldCount <= newCount) { changed = true; From 322b3b677ffd8fe893c6a94fbaf60768add095cc Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sat, 26 Oct 2024 19:51:11 +0200 Subject: [PATCH 032/154] fix: primitive 14: improper validation of outbox, followers, following & shared inbox collections --- .../src/core/activitypub/models/ApPersonService.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 2046dad099..97b4dd27c9 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -154,13 +154,24 @@ export class ApPersonService implements OnModuleInit { throw new Error('invalid Actor: inbox has different host'); } + const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); + if (sharedInboxObject != null) { + const sharedInbox = getApId(sharedInboxObject); + if (!(typeof sharedInbox === "string" && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) { + throw new Error("invalid Actor: wrong shared inbox"); + } + } + for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) { - const collectionUri = (x as IActor)[collection]; + const collectionUri = getApId((x as IActor)[collection]); if (typeof collectionUri === 'string' && collectionUri.length > 0) { if (this.utilityService.punyHost(collectionUri) !== expectHost) { throw new Error(`invalid Actor: ${collection} has different host`); } } + else if (collectionUri != null) { + throw new Error(`invalid Actor: wrong ${collection}`); + } } if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { From 4c432c07cbea935e06a839d897c3728c8964200d Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 20:21:17 -0500 Subject: [PATCH 033/154] fix: code style for primitive 14 --- .../backend/src/core/activitypub/models/ApPersonService.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 97b4dd27c9..8ddd646f05 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -157,8 +157,8 @@ export class ApPersonService implements OnModuleInit { const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); if (sharedInboxObject != null) { const sharedInbox = getApId(sharedInboxObject); - if (!(typeof sharedInbox === "string" && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) { - throw new Error("invalid Actor: wrong shared inbox"); + if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) { + throw new Error('invalid Actor: wrong shared inbox'); } } @@ -168,8 +168,7 @@ export class ApPersonService implements OnModuleInit { if (this.utilityService.punyHost(collectionUri) !== expectHost) { throw new Error(`invalid Actor: ${collection} has different host`); } - } - else if (collectionUri != null) { + } else if (collectionUri != null) { throw new Error(`invalid Actor: wrong ${collection}`); } } From ebea1a296228fb2a7694e9090e4fa8080cbaa1ec Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 05:07:58 +0200 Subject: [PATCH 034/154] fix: primitive 15: improper same-origin validation for note uri and url --- .../core/activitypub/models/ApNoteService.ts | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index f404a77fbb..146ccb11a2 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -141,14 +141,24 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - if (note.id && !checkHttps(note.id)) { + if (note.id == null) { + throw new Error('Refusing to create note without id'); + } + + if (!checkHttps(note.id)) { throw new Error('unexpected schema of note.id: ' + note.id); } const url = getOneApHrefNullable(note.url); - if (url && !checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + if (url != null) { + if (!checkHttps(url)) { + throw new Error('unexpected schema of note url: ' + url); + } + + if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { + throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id}`); + } } this.logger.info(`Creating the Note: ${note.id}`); @@ -366,7 +376,11 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - if (note.id && !checkHttps(note.id)) { + if (note.id == null) { + throw new Error('Refusing to update note without id'); + } + + if (!checkHttps(note.id)) { throw new Error('unexpected schema of note.id: ' + note.id); } @@ -376,6 +390,16 @@ export class ApNoteService { throw new Error('unexpected schema of note url: ' + url); } + if (url != null) { + if (!checkHttps(url)) { + throw new Error('unexpected schema of note url: ' + url); + } + + if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { + throw new Error(`note url <> id host mismatch: ${url} <> ${note.id}`); + } + } + this.logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ From b74e2e91674ee56ef0b835daa31f5a72d02ab37d Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 05:11:16 +0200 Subject: [PATCH 035/154] fix: primitive 16: improper same-origin validation for user uri and url --- .../activitypub/models/ApPersonService.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 8ddd646f05..7a3bd57d43 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -337,8 +337,18 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); - if (url && !checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); + if (person.id == null) { + throw new Error('Refusing to create person without id'); + } + + if (url != null) { + if (!checkHttps(url)) { + throw new Error('unexpected schema of person url: ' + url); + } + + if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { + throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`); + } } // Create user @@ -539,8 +549,18 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); - if (url && !checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); + if (person.id == null) { + throw new Error('Refusing to update person without id'); + } + + if (url != null) { + if (!checkHttps(url)) { + throw new Error('unexpected schema of person url: ' + url); + } + + if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { + throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`); + } } const updates = { From 4d925fc08683a9415c9488b5bcc516ca8f43d4af Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 04:18:49 +0200 Subject: [PATCH 036/154] fix: primitive 17: note same-origin identifier validation can be bypassed by wrapping the id in an array --- packages/backend/src/core/activitypub/ApInboxService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index edd1041062..b5a97d34c4 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -426,6 +426,9 @@ export class ApInboxService { return 'skip: host in actor.uri !== note.id'; } } + else { + return 'skip: note.id is not a string' + } } const unlock = await this.appLockService.getApLock(uri); From b9080da75dea7e9c5d38976814118a594be9e019 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 20:28:50 -0500 Subject: [PATCH 037/154] fix: code style for primitive 17 --- packages/backend/src/core/activitypub/ApInboxService.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index b5a97d34c4..42c2007799 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -425,9 +425,8 @@ export class ApInboxService { if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) { return 'skip: host in actor.uri !== note.id'; } - } - else { - return 'skip: note.id is not a string' + } else { + return 'skip: note.id is not a string'; } } From c04f34404991d69103703d781f52658fe3214d15 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 21:17:30 -0500 Subject: [PATCH 038/154] fix: primitive 13: check attribution against actor in notes --- .../src/core/activitypub/ApInboxService.ts | 4 +- .../core/activitypub/models/ApNoteService.ts | 71 ++++++++++++------- .../src/server/api/endpoints/ap/show.ts | 2 +- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 42c2007799..0f56ad3f78 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -436,7 +436,7 @@ export class ApInboxService { const exist = await this.apNoteService.fetchNote(note); if (exist) return 'skip: note exists'; - await this.apNoteService.createNote(note, resolver, silent); + await this.apNoteService.createNote(note, actor, resolver, silent); return 'ok'; } catch (err) { if (err instanceof StatusError && !err.isRetryable) { @@ -791,7 +791,7 @@ export class ApInboxService { await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); return 'ok: Question updated'; } else if (getApType(object) === 'Note') { - await this.apNoteService.updateNote(object, resolver).catch(err => console.error(err)); + await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); return 'ok: Note updated'; } else { return `skip: Unknown type: ${getApType(object)}`; diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 146ccb11a2..7857bcc28c 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -6,7 +6,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; +import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -49,6 +49,9 @@ export class ApNoteService { @Inject(DI.meta) private meta: MiMeta, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -82,7 +85,13 @@ export class ApNoteService { } @bindThis - public validateNote(object: IObject, uri: string): Error | null { + public validateNote( + object: IObject, + uri: string, + actor?: MiRemoteUser, + user?: MiRemoteUser, + note?: MiNote, + ): Error | null { const expectHost = this.utilityService.extractDbHost(uri); const apType = getApType(object); @@ -99,10 +108,27 @@ export class ApNoteService { return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } + if (actor) { + const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri; + if (attribution !== actor.uri) { + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`); + } + if (user && attribution !== user.uri) { + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated attribution does not match original attribution. updated attribution: ${user.uri}, original attribution: ${attribution}`); + } + } + if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed'); } + if (note) { + const url = (object.url) ? getOneApId(object.url) : note.url; + if (url && url !== note.url) { + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated url does not match original url. updated url: ${url}, original url: ${note.url}`); + } + } + return null; } @@ -120,14 +146,14 @@ export class ApNoteService { * Noteを作成します。 */ @bindThis - public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { + public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise { // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(value); const entryUri = getApId(value); - const err = this.validateNote(object, entryUri); + const err = this.validateNote(object, entryUri, actor); if (err) { this.logger.error(err.message, { resolver: { history: resolver.getHistory() }, @@ -171,8 +197,9 @@ export class ApNoteService { const uri = getOneApId(note.attributedTo); // ローカルで投稿者を検索し、もし凍結されていたらスキップ - const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser; - if (cachedActor && cachedActor.isSuspended) { + // eslint-disable-next-line no-param-reassign + actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined; + if (actor && actor.isSuspended) { throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); } @@ -204,7 +231,8 @@ export class ApNoteService { } //#endregion - const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; + // eslint-disable-next-line no-param-reassign + actor ??= await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; // 解決した投稿者が凍結されていたらスキップ if (actor.isSuspended) { @@ -345,7 +373,7 @@ export class ApNoteService { * Noteを作成します。 */ @bindThis - public async updateNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { + public async updateNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise { const noteUri = typeof value === 'string' ? value : value.id; if (noteUri == null) throw new Error('uri is null'); @@ -356,6 +384,9 @@ export class ApNoteService { const UpdatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); if (UpdatedNote == null) throw new Error('Note is not registered'); + const user = await this.usersRepository.findOneBy({ id: UpdatedNote.userId }) as MiRemoteUser | null; + if (user == null) throw new Error('Note is not registered'); + // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); @@ -372,6 +403,10 @@ export class ApNoteService { throw err; } + // `validateNote` checks that the actor and user are one and the same + // eslint-disable-next-line no-param-reassign + actor ??= user; + const note = object as IPost; this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); @@ -402,16 +437,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); - // 投稿者をフェッチ - if (note.attributedTo == null) { - throw new Error('invalid note.attributedTo: ' + note.attributedTo); - } - - const uri = getOneApId(note.attributedTo); - - // ローカルで投稿者を検索し、もし凍結されていたらスキップ - const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser; - if (cachedActor && cachedActor.isSuspended) { + if (actor.isSuspended) { throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); } @@ -443,13 +469,6 @@ export class ApNoteService { } //#endregion - const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; - - // 投稿者が凍結されていたらスキップ - if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); - } - const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); let visibility = noteAudience.visibility; const visibleUsers = noteAudience.visibleUsers; @@ -610,7 +629,7 @@ export class ApNoteService { // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri; - return await this.createNote(createFrom, options.resolver, true); + return await this.createNote(createFrom, undefined, options.resolver, true); } finally { unlock(); } diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index a877d1ce0d..4232bc6e39 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -140,7 +140,7 @@ export default class extends Endpoint { // eslint- return await this.mergePack( me, isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null, - isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null, + isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, undefined, true) : null, ); } From cbf8cc376e02e457a96d680dbbf0c110137d55f5 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 21:23:27 -0500 Subject: [PATCH 039/154] fix: primitive 18: `ap/get` bypasses access checks One might argue that we could make this one actually preform access checks against the returned activity object, but I feel like that's a lot more work than just restricting it to administrators, since, to me at least, it seems more like a debugging tool than anything else. --- packages/backend/src/server/api/endpoints/ap/get.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index d8c55de7ec..14286bc23e 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -11,6 +11,7 @@ import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; export const meta = { tags: ['federation'], + requireAdmin: true, requireCredential: true, kind: 'read:federation', From 408e782507837da4c9b2164266d6f6f3e48d1642 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 21:38:17 -0500 Subject: [PATCH 040/154] fix: primitive 19 & 20: respect blocks and hide more Ideally, the user property should also be hidden (as leaving it in leaks information slightly), but given the schema of the note endpoint, I don't think that would be possible without introducing some kind of "ghost" user, who is attributed for posts by users who have you blocked. --- .../src/core/entities/NoteEntityService.ts | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 4dd17c5af3..2855ae78f7 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -11,7 +11,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; +import type { BlockingsRepository, UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; @@ -39,6 +39,9 @@ export class NoteEntityService implements OnModuleInit { @Inject(DI.meta) private meta: MiMeta, + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -142,6 +145,17 @@ export class NoteEntityService implements OnModuleInit { } } + if (!hide && meId && packedNote.userId !== meId) { + const isBlocked = await this.blockingsRepository.exists({ + where: { + blockeeId: meId, + blockerId: packedNote.userId, + }, + }); + + if (isBlocked) hide = true; + } + if (hide) { packedNote.visibleUserIds = undefined; packedNote.fileIds = []; @@ -149,6 +163,12 @@ export class NoteEntityService implements OnModuleInit { packedNote.text = null; packedNote.poll = undefined; packedNote.cw = null; + packedNote.repliesCount = 0; + packedNote.reactionAcceptance = null; + packedNote.reactionAndUserPairCache = undefined; + packedNote.reactionCount = 0; + packedNote.reactionEmojis = undefined; + packedNote.reactions = undefined; packedNote.isHidden = true; } } @@ -262,7 +282,13 @@ export class NoteEntityService implements OnModuleInit { return true; } else { // フォロワーかどうか - const [following, user] = await Promise.all([ + const [blocked, following, user] = await Promise.all([ + this.blockingsRepository.exists({ + where: { + blockeeId: meId, + blockerId: note.userId, + }, + }), this.followingsRepository.count({ where: { followeeId: note.userId, @@ -273,6 +299,8 @@ export class NoteEntityService implements OnModuleInit { this.usersRepository.findOneByOrFail({ id: meId }), ]); + if (blocked) return false; + /* If we know the following, everyhting is fine. But if we do not know the following, it might be that both the @@ -284,6 +312,17 @@ export class NoteEntityService implements OnModuleInit { } } + if (meId != null) { + const isBlocked = await this.blockingsRepository.exists({ + where: { + blockeeId: meId, + blockerId: note.userId, + }, + }); + + if (isBlocked) return false; + } + return true; } From 74565f67f77085c2885ece50e6a79b50548029df Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 21:53:16 -0500 Subject: [PATCH 041/154] fix: primitives 21, 22, and 23: reuse resolver This also increases the default `recursionLimit` for `Resolver`, as it theoretically will go higher that it previously would and could possibly fail on non-malicious collection activities. --- .../src/core/activitypub/ApInboxService.ts | 85 +++++++++++-------- .../src/core/activitypub/ApResolverService.ts | 7 +- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 0f56ad3f78..90444a1af3 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -93,19 +93,26 @@ export class ApInboxService { } @bindThis - public async performActivity(actor: MiRemoteUser, activity: IObject): Promise { + public async performActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise { 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)) { + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); + + const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems); + if (items.length >= resolver.getRecursionLimit()) { + throw new Error(`skipping activity: collection would surpass recursion limit: ${this.utilityService.extractDbHost(actor.uri)}`); + } + + for (const item of items) { const act = await resolver.resolve(item); if (act.id == null || this.utilityService.extractDbHost(act.id) !== this.utilityService.extractDbHost(actor.uri)) { this.logger.debug('skipping activity: activity id is null or mismatching'); continue; } try { - results.push([getApId(item), await this.performOneActivity(actor, act)]); + results.push([getApId(item), await this.performOneActivity(actor, act, resolver)]); } catch (err) { if (err instanceof Error || typeof err === 'string') { this.logger.error(err); @@ -120,7 +127,7 @@ export class ApInboxService { result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n'); } } else { - result = await this.performOneActivity(actor, activity); + result = await this.performOneActivity(actor, activity, resolver); } // ついでにリモートユーザーの情報が古かったら更新しておく @@ -135,37 +142,37 @@ export class ApInboxService { } @bindThis - public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise { + public async performOneActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise { if (actor.isSuspended) return; if (isCreate(activity)) { - return await this.create(actor, activity); + return await this.create(actor, activity, resolver); } else if (isDelete(activity)) { return await this.delete(actor, activity); } else if (isUpdate(activity)) { - return await this.update(actor, activity); + return await this.update(actor, activity, resolver); } else if (isFollow(activity)) { return await this.follow(actor, activity); } else if (isAccept(activity)) { - return await this.accept(actor, activity); + return await this.accept(actor, activity, resolver); } else if (isReject(activity)) { - return await this.reject(actor, activity); + return await this.reject(actor, activity, resolver); } else if (isAdd(activity)) { - return await this.add(actor, activity); + return await this.add(actor, activity, resolver); } else if (isRemove(activity)) { - return await this.remove(actor, activity); + return await this.remove(actor, activity, resolver); } else if (isAnnounce(activity)) { - return await this.announce(actor, activity); + return await this.announce(actor, activity, resolver); } else if (isLike(activity)) { return await this.like(actor, activity); } else if (isUndo(activity)) { - return await this.undo(actor, activity); + return await this.undo(actor, activity, resolver); } else if (isBlock(activity)) { return await this.block(actor, activity); } else if (isFlag(activity)) { return await this.flag(actor, activity); } else if (isMove(activity)) { - return await this.move(actor, activity); + return await this.move(actor, activity, resolver); } else { return `unrecognized activity type: ${activity.type}`; } @@ -207,12 +214,13 @@ export class ApInboxService { } @bindThis - private async accept(actor: MiRemoteUser, activity: IAccept): Promise { + private async accept(actor: MiRemoteUser, activity: IAccept, resolver?: Resolver): Promise { const uri = activity.id ?? activity; this.logger.info(`Accept: ${uri}`); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(err => { this.logger.error(`Resolution failed: ${err}`); @@ -249,7 +257,7 @@ export class ApInboxService { } @bindThis - private async add(actor: MiRemoteUser, activity: IAdd): Promise { + private async add(actor: MiRemoteUser, activity: IAdd, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'invalid actor'; } @@ -260,7 +268,7 @@ export class ApInboxService { if (activity.target === actor.featured) { const object = fromTuple(activity.object); - const note = await this.apNoteService.resolveNote(object); + const note = await this.apNoteService.resolveNote(object, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.addPinned(actor, note.id); return; @@ -270,12 +278,13 @@ export class ApInboxService { } @bindThis - private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise { + private async announce(actor: MiRemoteUser, activity: IAnnounce, resolver?: Resolver): Promise { const uri = getApId(activity); this.logger.info(`Announce: ${uri}`); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const activityObject = fromTuple(activity.object); if (!activityObject) return 'skip: activity has no object property'; @@ -293,7 +302,7 @@ export class ApInboxService { } @bindThis - private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise { + private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise { const uri = getApId(activity); if (actor.isSuspended) { @@ -315,7 +324,7 @@ export class ApInboxService { // Announce対象をresolve let renote; try { - renote = await this.apNoteService.resolveNote(target); + renote = await this.apNoteService.resolveNote(target, { resolver }); if (renote == null) return 'announce target is null'; } catch (err) { // 対象が4xxならスキップ @@ -334,7 +343,7 @@ export class ApInboxService { this.logger.info(`Creating the (Re)Note: ${uri}`); - const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc); + const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc, resolver); const createdAt = activity.published ? new Date(activity.published) : null; if (createdAt && createdAt < this.idService.parse(renote.id).date) { @@ -372,7 +381,7 @@ export class ApInboxService { } @bindThis - private async create(actor: MiRemoteUser, activity: ICreate): Promise { + private async create(actor: MiRemoteUser, activity: ICreate, resolver?: Resolver): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); @@ -398,7 +407,8 @@ export class ApInboxService { activityObject.attributedTo = activity.actor; } - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -574,12 +584,13 @@ export class ApInboxService { } @bindThis - private async reject(actor: MiRemoteUser, activity: IReject): Promise { + private async reject(actor: MiRemoteUser, activity: IReject, resolver?: Resolver): Promise { const uri = activity.id ?? activity; this.logger.info(`Reject: ${uri}`); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -616,7 +627,7 @@ export class ApInboxService { } @bindThis - private async remove(actor: MiRemoteUser, activity: IRemove): Promise { + private async remove(actor: MiRemoteUser, activity: IRemove, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'invalid actor'; } @@ -627,7 +638,7 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); - const note = await this.apNoteService.resolveNote(activityObject); + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.removePinned(actor, note.id); return; @@ -637,7 +648,7 @@ export class ApInboxService { } @bindThis - private async undo(actor: MiRemoteUser, activity: IUndo): Promise { + private async undo(actor: MiRemoteUser, activity: IUndo, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'invalid actor'; } @@ -646,7 +657,8 @@ export class ApInboxService { this.logger.info(`Undo: ${uri}`); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -770,14 +782,15 @@ export class ApInboxService { } @bindThis - private async update(actor: MiRemoteUser, activity: IUpdate): Promise { + private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } this.logger.debug('Update'); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -799,11 +812,11 @@ export class ApInboxService { } @bindThis - private async move(actor: MiRemoteUser, activity: IMove): Promise { + private async move(actor: MiRemoteUser, activity: IMove, resolver?: Resolver): Promise { // fetch the new and old accounts const targetUri = getApHrefNullable(activity.target); if (!targetUri) return 'skip: invalid activity target'; - return await this.apPersonService.updatePerson(actor.uri) ?? 'skip: nothing to do'; + return await this.apPersonService.updatePerson(actor.uri, resolver) ?? 'skip: nothing to do'; } } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index a2c7ed19d8..25ccbdac60 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -42,7 +42,7 @@ export class Resolver { private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, private loggerService: LoggerService, - private recursionLimit = 100, + private recursionLimit = 256, ) { this.history = new Set(); this.logger = this.loggerService.getLogger('ap-resolve'); @@ -53,6 +53,11 @@ export class Resolver { return Array.from(this.history); } + @bindThis + public getRecursionLimit(): number { + return this.recursionLimit; + } + @bindThis public async resolveCollection(value: string | IObject): Promise { const collection = typeof value === 'string' From 5764fa55cb7cb404fed3d029b658c495ec52ecaf Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 22:01:22 -0500 Subject: [PATCH 042/154] fix: primitives 25-33: proper local instance checks --- packages/backend/src/core/RemoteUserResolveService.ts | 4 ++-- packages/backend/src/core/UtilityService.ts | 5 +++++ .../backend/src/core/activitypub/ApDbResolverService.ts | 6 +++++- .../backend/src/core/activitypub/models/ApNoteService.ts | 2 +- .../src/core/activitypub/models/ApPersonService.ts | 9 ++++----- .../src/core/activitypub/models/ApQuestionService.ts | 4 +++- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index f5a55eb8bc..678da0cfa6 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -54,9 +54,9 @@ export class RemoteUserResolveService { }) as MiLocalUser; } - host = this.utilityService.toPuny(host); + host = this.utilityService.punyHost(host); - if (this.config.host === host) { + if (host === this.utilityService.toPuny(this.config.host)) { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { if (u == null) { diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 009dd4665f..4c6d539e16 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -34,6 +34,11 @@ export class UtilityService { return this.toPuny(this.config.host) === this.toPuny(host); } + @bindThis + public isUriLocal(uri: string): boolean { + return this.punyHost(uri) === this.toPuny(this.config.host); + } + @bindThis public isBlockedHost(blockedHosts: string[], host: string | null): boolean { if (host == null) return false; diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 8c97cc8ce8..dd89716d34 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -10,6 +10,7 @@ import type { Config } from '@/config.js'; import { MemoryKVCache } from '@/misc/cache.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; import { CacheService } from '@/core/CacheService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; @@ -55,6 +56,7 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, private apLoggerService: ApLoggerService, + private utilityService: UtilityService, ) { this.publicKeyCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h this.publicKeyByUserIdCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h @@ -65,7 +67,9 @@ export class ApDbResolverService implements OnApplicationShutdown { const separator = '/'; const uri = new URL(getApId(value)); - if (uri.origin !== this.config.url) return { local: false, uri: uri.href }; + if (this.utilityService.toPuny(uri.host) !== this.utilityService.toPuny(this.config.host)) { + return { local: false, uri: uri.href }; + } const [, type, id, ...rest] = uri.pathname.split(separator); return { diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 7857bcc28c..a0ddc2075b 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -621,7 +621,7 @@ export class ApNoteService { if (exist) return exist; //#endregion - if (uri.startsWith(this.config.url)) { + if (this.utilityService.isUriLocal(uri)) { throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 7a3bd57d43..1c117795e9 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -296,7 +296,8 @@ export class ApPersonService implements OnModuleInit { public async createPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); - if (uri.startsWith(this.config.url)) { + const host = this.utilityService.punyHost(uri); + if (host === this.utilityService.toPuny(this.config.host)) { throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); } @@ -310,8 +311,6 @@ export class ApPersonService implements OnModuleInit { this.logger.info(`Creating the Person: ${person.id}`); - const host = this.utilityService.punyHost(object.id); - const fields = this.analyzeAttachments(person.attachment ?? []); const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); @@ -500,7 +499,7 @@ export class ApPersonService implements OnModuleInit { if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(`${this.config.url}/`)) return; + if (this.utilityService.isUriLocal(uri)) return; //#region このサーバーに既に登録されているか const exist = await this.fetchPerson(uri) as MiRemoteUser | null; @@ -777,7 +776,7 @@ export class ApPersonService implements OnModuleInit { await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]); dst = await this.fetchPerson(src.movedToUri) ?? dst; } else { - if (src.movedToUri.startsWith(`${this.config.url}/`)) { + if (this.utilityService.isUriLocal(src.movedToUri)) { // ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている return 'failed: movedTo is local but not found'; } diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index c1aea15ece..83a98d17f9 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -11,6 +11,7 @@ import type { IPoll } from '@/models/Poll.js'; import type { MiRemoteUser } from '@/models/User.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { UtilityService } from '@/core/UtilityService.js'; import { getOneApId, isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; @@ -36,6 +37,7 @@ export class ApQuestionService { private apResolverService: ApResolverService, private apLoggerService: ApLoggerService, + private utilityService: UtilityService, ) { this.logger = this.apLoggerService.logger; } @@ -74,7 +76,7 @@ export class ApQuestionService { if (uri == null) throw new Error('uri is null'); // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local'); + if (this.utilityService.isUriLocal(uri)) throw new Error('uri points local'); //#region このサーバーに既に登録されているか const note = await this.notesRepository.findOneBy({ uri }); From cc4e99fdde690cea82c45f0ed9595b78810a1630 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 14 Nov 2024 23:43:19 -0500 Subject: [PATCH 043/154] fix: Try using `CacheService` to avoid excess db lookups This isn't perfect, theoretically if some massive number of users blocked the user making this request the set lookup could take a long amount of time, but eh, it works, and that scenario is highly unlikely. --- .../src/core/entities/NoteEntityService.ts | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 2855ae78f7..985245aeb1 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -11,12 +11,13 @@ import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; -import type { BlockingsRepository, UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import type { OnModuleInit } from '@nestjs/common'; +import type { CacheService } from '../CacheService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; @@ -27,6 +28,7 @@ import type { Config } from '@/config.js'; export class NoteEntityService implements OnModuleInit { private userEntityService: UserEntityService; private driveFileEntityService: DriveFileEntityService; + private cacheService: CacheService; private customEmojiService: CustomEmojiService; private reactionService: ReactionService; private reactionsBufferingService: ReactionsBufferingService; @@ -39,9 +41,6 @@ export class NoteEntityService implements OnModuleInit { @Inject(DI.meta) private meta: MiMeta, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -78,6 +77,7 @@ export class NoteEntityService implements OnModuleInit { onModuleInit() { this.userEntityService = this.moduleRef.get('UserEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); + this.cacheService = this.moduleRef.get('CacheService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.reactionService = this.moduleRef.get('ReactionService'); this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService'); @@ -146,12 +146,7 @@ export class NoteEntityService implements OnModuleInit { } if (!hide && meId && packedNote.userId !== meId) { - const isBlocked = await this.blockingsRepository.exists({ - where: { - blockeeId: meId, - blockerId: packedNote.userId, - }, - }); + const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(packedNote.userId); if (isBlocked) hide = true; } @@ -283,12 +278,7 @@ export class NoteEntityService implements OnModuleInit { } else { // フォロワーかどうか const [blocked, following, user] = await Promise.all([ - this.blockingsRepository.exists({ - where: { - blockeeId: meId, - blockerId: note.userId, - }, - }), + this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)), this.followingsRepository.count({ where: { followeeId: note.userId, @@ -313,12 +303,7 @@ export class NoteEntityService implements OnModuleInit { } if (meId != null) { - const isBlocked = await this.blockingsRepository.exists({ - where: { - blockeeId: meId, - blockerId: note.userId, - }, - }); + const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(note.userId); if (isBlocked) return false; } From f36f4b5398561dcf7365729c530f5b1868c6b994 Mon Sep 17 00:00:00 2001 From: rectcoordsystem Date: Wed, 6 Nov 2024 05:31:11 +0900 Subject: [PATCH 044/154] fix(backend): check target IP before sending HTTP request --- packages/backend/src/core/DownloadService.ts | 22 ----- .../backend/src/core/HttpRequestService.ts | 90 ++++++++++++++++++- 2 files changed, 88 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 0e992f05de..05b9e64a37 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -6,7 +6,6 @@ import * as fs from 'node:fs'; import * as stream from 'node:stream/promises'; import { Inject, Injectable } from '@nestjs/common'; -import ipaddr from 'ipaddr.js'; import chalk from 'chalk'; import got, * as Got from 'got'; import { parse } from 'content-disposition'; @@ -70,13 +69,6 @@ export class DownloadService { }, enableUnixSockets: false, }).on('response', (res: Got.Response) => { - if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { - if (this.isPrivateIp(res.ip)) { - this.logger.warn(`Blocked address: ${res.ip}`); - req.destroy(); - } - } - const contentLength = res.headers['content-length']; if (contentLength != null) { const size = Number(contentLength); @@ -139,18 +131,4 @@ export class DownloadService { cleanup(); } } - - @bindThis - private isPrivateIp(ip: string): boolean { - const parsedIp = ipaddr.parse(ip); - - for (const net of this.config.allowedPrivateNetworks ?? []) { - const cidr = ipaddr.parseCIDR(net); - if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { - return false; - } - } - - return parsedIp.range() !== 'unicast'; - } } diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 08e9f46b2d..6c013eacc0 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -6,6 +6,7 @@ import * as http from 'node:http'; import * as https from 'node:https'; import * as net from 'node:net'; +import ipaddr from 'ipaddr.js'; import CacheableLookup from 'cacheable-lookup'; import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; @@ -25,6 +26,91 @@ export type HttpRequestSendOptions = { validators?: ((res: Response) => void)[]; }; +@Injectable() +class HttpRequestServiceAgent extends http.Agent { + constructor( + @Inject(DI.config) + private config: Config, + + options?: Object + ) { + super(options); + } + + @bindThis + public createConnection(options: Object, callback?: Function): net.Socket { + const socket = super.createConnection(options, callback) + .on('connect', ()=>{ + const address = socket.remoteAddress; + if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') { + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } + } + } + }); + return socket; + }; + + @bindThis + private isPrivateIp(ip: string): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const net of this.config.allowedPrivateNetworks ?? []) { + const cidr = ipaddr.parseCIDR(net); + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { + return false; + } + } + + return parsedIp.range() !== 'unicast'; + } +} + +@Injectable() +class HttpsRequestServiceAgent extends https.Agent { + constructor( + @Inject(DI.config) + private config: Config, + + options?: Object + ) { + super(options); + } + + @bindThis + public createConnection(options: Object, callback?: Function): net.Socket { + const socket = super.createConnection(options, callback) + .on('connect', ()=>{ + const address = socket.remoteAddress; + if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') { + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } + } + } + }); + return socket; + }; + + @bindThis + private isPrivateIp(ip: string): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const net of this.config.allowedPrivateNetworks ?? []) { + const cidr = ipaddr.parseCIDR(net); + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { + return false; + } + } + + return parsedIp.range() !== 'unicast'; + } +} + + @Injectable() export class HttpRequestService { /** @@ -57,14 +143,14 @@ export class HttpRequestService { lookup: false, // nativeのdns.lookupにfallbackしない }); - this.http = new http.Agent({ + this.http = new HttpRequestServiceAgent(config, { keepAlive: true, keepAliveMsecs: 30 * 1000, lookup: cache.lookup as unknown as net.LookupFunction, localAddress: config.outgoingAddress, }); - this.https = new https.Agent({ + this.https = new HttpsRequestServiceAgent(config, { keepAlive: true, keepAliveMsecs: 30 * 1000, lookup: cache.lookup as unknown as net.LookupFunction, From 7ccccf5545c1292c8de8b24bc426b1f9430528ef Mon Sep 17 00:00:00 2001 From: rectcoordsystem Date: Wed, 6 Nov 2024 06:33:44 +0900 Subject: [PATCH 045/154] fix(backend): allow accessing private IP when testing --- packages/backend/src/core/HttpRequestService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 6c013eacc0..8c5e3bdb91 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -42,7 +42,7 @@ class HttpRequestServiceAgent extends http.Agent { const socket = super.createConnection(options, callback) .on('connect', ()=>{ const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') { + if (process.env.NODE_ENV === 'production') { if (address && ipaddr.isValid(address)) { if (this.isPrivateIp(address)) { socket.destroy(new Error(`Blocked address: ${address}`)); @@ -84,7 +84,7 @@ class HttpsRequestServiceAgent extends https.Agent { const socket = super.createConnection(options, callback) .on('connect', ()=>{ const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') { + if (process.env.NODE_ENV === 'production') { if (address && ipaddr.isValid(address)) { if (this.isPrivateIp(address)) { socket.destroy(new Error(`Blocked address: ${address}`)); From 663c06be00d5b05d6c3e96559b170190805f38ed Mon Sep 17 00:00:00 2001 From: rectcoordsystem <37621004+rectcoordsystem@users.noreply.github.com> Date: Wed, 13 Nov 2024 03:06:22 +0900 Subject: [PATCH 046/154] Apply suggestions from code review Co-authored-by: anatawa12 --- .../backend/src/core/HttpRequestService.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 8c5e3bdb91..0c8b5b4ef4 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -26,30 +26,33 @@ export type HttpRequestSendOptions = { validators?: ((res: Response) => void)[]; }; -@Injectable() +declare module 'node:http' { + interface Agent { + createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket; + } +} + class HttpRequestServiceAgent extends http.Agent { constructor( - @Inject(DI.config) private config: Config, - - options?: Object + options?: http.AgentOptions, ) { super(options); } @bindThis - public createConnection(options: Object, callback?: Function): net.Socket { + public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { const socket = super.createConnection(options, callback) - .on('connect', ()=>{ - const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); + .on('connect', () => { + const address = socket.remoteAddress; + if (process.env.NODE_ENV === 'production') { + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } } } - } - }); + }); return socket; }; @@ -68,13 +71,10 @@ class HttpRequestServiceAgent extends http.Agent { } } -@Injectable() class HttpsRequestServiceAgent extends https.Agent { constructor( - @Inject(DI.config) private config: Config, - - options?: Object + options?: https.AgentOptions, ) { super(options); } From 360d71278a78169c2b2351bbcf518c1e1654b254 Mon Sep 17 00:00:00 2001 From: rectcoordsystem Date: Wed, 13 Nov 2024 03:27:52 +0900 Subject: [PATCH 047/154] fix(backend): lint and typecheck --- .../backend/src/core/HttpRequestService.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 0c8b5b4ef4..0955d1f5bb 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -80,18 +80,18 @@ class HttpsRequestServiceAgent extends https.Agent { } @bindThis - public createConnection(options: Object, callback?: Function): net.Socket { + public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { const socket = super.createConnection(options, callback) - .on('connect', ()=>{ - const address = socket.remoteAddress; - if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); + .on('connect', () => { + const address = socket.remoteAddress; + if (process.env.NODE_ENV === 'production') { + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } } } - } - }); + }); return socket; }; @@ -110,7 +110,6 @@ class HttpsRequestServiceAgent extends https.Agent { } } - @Injectable() export class HttpRequestService { /** From 7b3e3f8e25a09430e250096ee01c0f913840623f Mon Sep 17 00:00:00 2001 From: rectcoordsystem Date: Wed, 13 Nov 2024 13:30:01 +0900 Subject: [PATCH 048/154] fix(backend): add isLocalAddressAllowed option to getAgentByUrl and send (HttpRequestService) --- .../backend/src/core/HttpRequestService.ts | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 0955d1f5bb..0ad5667049 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -112,6 +112,16 @@ class HttpsRequestServiceAgent extends https.Agent { @Injectable() export class HttpRequestService { + /** + * Get http non-proxy agent (without local address filtering) + */ + private httpNative: http.Agent; + + /** + * Get https non-proxy agent (without local address filtering) + */ + private httpsNative: https.Agent; + /** * Get http non-proxy agent */ @@ -142,19 +152,20 @@ export class HttpRequestService { lookup: false, // nativeのdns.lookupにfallbackしない }); - this.http = new HttpRequestServiceAgent(config, { + const agentOption = { keepAlive: true, keepAliveMsecs: 30 * 1000, lookup: cache.lookup as unknown as net.LookupFunction, localAddress: config.outgoingAddress, - }); + }; - this.https = new HttpsRequestServiceAgent(config, { - keepAlive: true, - keepAliveMsecs: 30 * 1000, - lookup: cache.lookup as unknown as net.LookupFunction, - localAddress: config.outgoingAddress, - }); + this.httpNative = new http.Agent(agentOption); + + this.httpsNative = new https.Agent(agentOption); + + this.http = new HttpRequestServiceAgent(config, agentOption); + + this.https = new HttpsRequestServiceAgent(config, agentOption); const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); @@ -189,16 +200,22 @@ export class HttpRequestService { * @param bypassProxy Allways bypass proxy */ @bindThis - public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent { + public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent { if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) { + if (isLocalAddressAllowed) { + return url.protocol === 'http:' ? this.httpNative : this.httpsNative; + } return url.protocol === 'http:' ? this.http : this.https; } else { + if (isLocalAddressAllowed && (!this.config.proxy)) { + return url.protocol === 'http:' ? this.httpNative : this.httpsNative; + } return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent; } } @bindThis - public async getActivityJson(url: string): Promise { + public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise { const res = await this.send(url, { method: 'GET', headers: { @@ -206,6 +223,7 @@ export class HttpRequestService { }, timeout: 5000, size: 1024 * 256, + isLocalAddressAllowed: isLocalAddressAllowed, }, { throwErrorWhenResponseNotOk: true, validators: [validateContentTypeSetAsActivityPub], @@ -220,7 +238,7 @@ export class HttpRequestService { } @bindThis - public async getJson(url: string, accept = 'application/json, */*', headers?: Record): Promise { + public async getJson(url: string, accept = 'application/json, */*', headers?: Record, isLocalAddressAllowed = false): Promise { const res = await this.send(url, { method: 'GET', headers: Object.assign({ @@ -228,19 +246,21 @@ export class HttpRequestService { }, headers ?? {}), timeout: 5000, size: 1024 * 256, + isLocalAddressAllowed: isLocalAddressAllowed, }); return await res.json() as T; } @bindThis - public async getHtml(url: string, accept = 'text/html, */*', headers?: Record): Promise { + public async getHtml(url: string, accept = 'text/html, */*', headers?: Record, isLocalAddressAllowed = false): Promise { const res = await this.send(url, { method: 'GET', headers: Object.assign({ Accept: accept, }, headers ?? {}), timeout: 5000, + isLocalAddressAllowed: isLocalAddressAllowed, }); return await res.text(); @@ -255,6 +275,7 @@ export class HttpRequestService { headers?: Record, timeout?: number, size?: number, + isLocalAddressAllowed?: boolean, } = {}, extra: HttpRequestSendOptions = { throwErrorWhenResponseNotOk: true, @@ -268,6 +289,8 @@ export class HttpRequestService { controller.abort(); }, timeout); + const isLocalAddressAllowed = args.isLocalAddressAllowed ?? false; + const res = await fetch(url, { method: args.method ?? 'GET', headers: { @@ -276,7 +299,7 @@ export class HttpRequestService { }, body: args.body, size: args.size ?? 10 * 1024 * 1024, - agent: (url) => this.getAgentByUrl(url), + agent: (url) => this.getAgentByUrl(url, false, isLocalAddressAllowed), signal: controller.signal, }); From 776f6fd1f568fc36e1b6b84d9b8e56611be58c5d Mon Sep 17 00:00:00 2001 From: rectcoordsystem Date: Wed, 13 Nov 2024 15:27:17 +0900 Subject: [PATCH 049/154] fix(backend): allow fetchSummaryFromProxy, trueMail to access local addresses --- packages/backend/src/core/EmailService.ts | 1 + packages/backend/src/server/web/UrlPreviewService.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index a176474b95..da198d0e42 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -312,6 +312,7 @@ export class EmailService { Accept: 'application/json', Authorization: truemailAuthKey, }, + isLocalAddressAllowed: true, }); const json = (await res.json()) as { diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 981fbb4353..47cc09b067 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -170,6 +170,6 @@ export class UrlPreviewService { contentLengthRequired: meta.urlPreviewRequireContentLength, }); - return this.httpRequestService.getJson(`${proxy}?${queryStr}`); + return this.httpRequestService.getJson(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true); } } From 8e90484b3e7ac255cc141000444e2ed6d6fa54f8 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 19:21:57 -0500 Subject: [PATCH 050/154] Bump version --- package.json | 2 +- .../backend/src/server/FileServerService.ts | 89 ------------------- 2 files changed, 1 insertion(+), 90 deletions(-) diff --git a/package.json b/package.json index 2ba3881756..76cfbadb23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.9.2", + "version": "2024.9.3", "codename": "shonk", "repository": { "type": "git", diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index be196373c4..1a4d0cb48f 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -28,11 +28,7 @@ import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; -import { RateLimiterService } from '@/server/api/RateLimiterService.js'; -import { getIpHash } from '@/misc/get-ip-hash.js'; -import { AuthenticateService } from '@/server/api/AuthenticateService.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; -import type Limiter from 'ratelimiter'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -56,8 +52,6 @@ export class FileServerService { private videoProcessingService: VideoProcessingService, private internalStorageService: InternalStorageService, private loggerService: LoggerService, - private authenticateService: AuthenticateService, - private rateLimiterService: RateLimiterService, ) { this.logger = this.loggerService.getLogger('server', 'gray'); @@ -82,8 +76,6 @@ export class FileServerService { }); fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { - if (!await this.checkRateLimit(request, reply, `/files/${request.params.key}`)) return; - return await this.sendDriveFile(request, reply) .catch(err => this.errorHandler(request, reply, err)); }); @@ -97,20 +89,6 @@ export class FileServerService { Params: { url: string; }; Querystring: { url?: string; }; }>('/proxy/:url*', async (request, reply) => { - const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url; - if (!url || !URL.canParse(url)) { - reply.code(400); - return; - } - - const keyUrl = new URL(url); - keyUrl.searchParams.forEach(k => keyUrl.searchParams.delete(k)); - keyUrl.hash = ''; - keyUrl.username = ''; - keyUrl.password = ''; - - if (!await this.checkRateLimit(request, reply, `/proxy/${keyUrl}`)) return; - return await this.proxyHandler(request, reply) .catch(err => this.errorHandler(request, reply, err)); }); @@ -594,71 +572,4 @@ export class FileServerService { path, }; } - - // Based on ApiCallService - private async checkRateLimit( - request: FastifyRequest<{ - Body?: Record | undefined, - Querystring?: Record | undefined, - Params?: Record | unknown, - }>, - reply: FastifyReply, - rateLimitKey: string, - ): Promise { - const body = request.method === 'GET' - ? request.query - : request.body; - - // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) - const token = request.headers.authorization?.startsWith('Bearer ') - ? request.headers.authorization.slice(7) - : body?.['i']; - if (token != null && typeof token !== 'string') { - reply.code(400); - return false; - } - - // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. - const [user] = await this.authenticateService.authenticate(token); - const actor = user?.id ?? getIpHash(request.ip); - - const limit = { - // Group by resource - key: rateLimitKey, - - // Maximum of 10 requests / 10 minutes - max: 10, - duration: 1000 * 60 * 10, - - // Minimum of 250 ms between each request - minInterval: 250, - }; - - // Rate limit proxy requests - try { - await this.rateLimiterService.limit(limit, actor); - return true; - } catch (err) { - // errはLimiter.LimiterInfoであることが期待される - reply.code(429); - - if (hasRateLimitInfo(err)) { - const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000); - // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく - reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10)); - } - - reply.send({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - }); - - return false; - } - } -} - -function hasRateLimitInfo(err: unknown): err is { info: Limiter.LimiterInfo } { - return err != null && typeof(err) === 'object' && 'info' in err; } From b0834ebf55d76979313378282d1dfc535b030e5d Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 19 Nov 2024 22:59:07 -0500 Subject: [PATCH 051/154] prevent DoS from spammed media proxy requests --- .../backend/src/server/FileServerService.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 1a4d0cb48f..be196373c4 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -28,7 +28,11 @@ import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; +import { RateLimiterService } from '@/server/api/RateLimiterService.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; +import { AuthenticateService } from '@/server/api/AuthenticateService.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; +import type Limiter from 'ratelimiter'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -52,6 +56,8 @@ export class FileServerService { private videoProcessingService: VideoProcessingService, private internalStorageService: InternalStorageService, private loggerService: LoggerService, + private authenticateService: AuthenticateService, + private rateLimiterService: RateLimiterService, ) { this.logger = this.loggerService.getLogger('server', 'gray'); @@ -76,6 +82,8 @@ export class FileServerService { }); fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { + if (!await this.checkRateLimit(request, reply, `/files/${request.params.key}`)) return; + return await this.sendDriveFile(request, reply) .catch(err => this.errorHandler(request, reply, err)); }); @@ -89,6 +97,20 @@ export class FileServerService { Params: { url: string; }; Querystring: { url?: string; }; }>('/proxy/:url*', async (request, reply) => { + const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url; + if (!url || !URL.canParse(url)) { + reply.code(400); + return; + } + + const keyUrl = new URL(url); + keyUrl.searchParams.forEach(k => keyUrl.searchParams.delete(k)); + keyUrl.hash = ''; + keyUrl.username = ''; + keyUrl.password = ''; + + if (!await this.checkRateLimit(request, reply, `/proxy/${keyUrl}`)) return; + return await this.proxyHandler(request, reply) .catch(err => this.errorHandler(request, reply, err)); }); @@ -572,4 +594,71 @@ export class FileServerService { path, }; } + + // Based on ApiCallService + private async checkRateLimit( + request: FastifyRequest<{ + Body?: Record | undefined, + Querystring?: Record | undefined, + Params?: Record | unknown, + }>, + reply: FastifyReply, + rateLimitKey: string, + ): Promise { + const body = request.method === 'GET' + ? request.query + : request.body; + + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : body?.['i']; + if (token != null && typeof token !== 'string') { + reply.code(400); + return false; + } + + // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. + const [user] = await this.authenticateService.authenticate(token); + const actor = user?.id ?? getIpHash(request.ip); + + const limit = { + // Group by resource + key: rateLimitKey, + + // Maximum of 10 requests / 10 minutes + max: 10, + duration: 1000 * 60 * 10, + + // Minimum of 250 ms between each request + minInterval: 250, + }; + + // Rate limit proxy requests + try { + await this.rateLimiterService.limit(limit, actor); + return true; + } catch (err) { + // errはLimiter.LimiterInfoであることが期待される + reply.code(429); + + if (hasRateLimitInfo(err)) { + const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000); + // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく + reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10)); + } + + reply.send({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + }); + + return false; + } + } +} + +function hasRateLimitInfo(err: unknown): err is { info: Limiter.LimiterInfo } { + return err != null && typeof(err) === 'object' && 'info' in err; } From fa3cf6c2996741e642955c5e2fca8ad785e83205 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 20:06:46 -0500 Subject: [PATCH 052/154] Fix type error in security fixes --- .../core/activitypub/models/ApPersonService.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 1c117795e9..2119c41569 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -163,13 +163,16 @@ export class ApPersonService implements OnModuleInit { } for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) { - const collectionUri = getApId((x as IActor)[collection]); - if (typeof collectionUri === 'string' && collectionUri.length > 0) { - if (this.utilityService.punyHost(collectionUri) !== expectHost) { - throw new Error(`invalid Actor: ${collection} has different host`); + const xCollection = (x as IActor)[collection]; + if (xCollection != null) { + const collectionUri = getApId(xCollection); + if (typeof collectionUri === 'string' && collectionUri.length > 0) { + if (this.utilityService.punyHost(collectionUri) !== expectHost) { + throw new Error(`invalid Actor: ${collection} has different host`); + } + } else if (collectionUri != null) { + throw new Error(`invalid Actor: wrong ${collection}`); } - } else if (collectionUri != null) { - throw new Error(`invalid Actor: wrong ${collection}`); } } From 1758f29364eca3cbd13dbb5c84909c93712b3b3b Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 20:16:43 -0500 Subject: [PATCH 053/154] Fix error in test function calls --- packages/backend/test/unit/activitypub.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 53ced3dab3..73d6186edf 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -176,7 +176,7 @@ describe('ActivityPub', () => { resolver.register(actor.id, actor); resolver.register(post.id, post); - const note = await noteService.createNote(post.id, resolver, true); + const note = await noteService.createNote(post.id, undefined, resolver, true); assert.deepStrictEqual(note?.uri, post.id); assert.deepStrictEqual(note.visibility, 'public'); @@ -336,7 +336,7 @@ describe('ActivityPub', () => { resolver.register(actor.featured, featured); resolver.register(firstNote.id, firstNote); - const note = await noteService.createNote(firstNote.id as string, resolver); + const note = await noteService.createNote(firstNote.id as string, undefined, resolver); assert.strictEqual(note?.uri, firstNote.id); }); }); From 23c4aa25714af145098baa7edd74c1d217e51c1a Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 20:24:59 -0500 Subject: [PATCH 054/154] Fix style error --- packages/backend/src/core/HttpRequestService.ts | 10 +++++----- .../src/queue/processors/InboxProcessorService.ts | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 0ad5667049..6dcd0cdff3 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -54,19 +54,19 @@ class HttpRequestServiceAgent extends http.Agent { } }); return socket; - }; + } @bindThis private isPrivateIp(ip: string): boolean { const parsedIp = ipaddr.parse(ip); - + for (const net of this.config.allowedPrivateNetworks ?? []) { const cidr = ipaddr.parseCIDR(net); if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { return false; } } - + return parsedIp.range() !== 'unicast'; } } @@ -98,14 +98,14 @@ class HttpsRequestServiceAgent extends https.Agent { @bindThis private isPrivateIp(ip: string): boolean { const parsedIp = ipaddr.parse(ip); - + for (const net of this.config.allowedPrivateNetworks ?? []) { const cidr = ipaddr.parseCIDR(net); if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { return false; } } - + return parsedIp.range() !== 'unicast'; } } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index f453d7d1ae..102e835e24 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -192,8 +192,7 @@ export class InboxProcessorService implements OnApplicationShutdown { if (signerHost !== activityIdHost) { throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`); } - } - else { + } else { throw new Bull.UnrecoverableError('skip: activity id is not a string'); } From 36af07abe28bec670aaebf9f5af5694bb582c29a Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 20:31:22 -0500 Subject: [PATCH 055/154] Fix another style error --- packages/backend/src/core/HttpRequestService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 6dcd0cdff3..083153940a 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -93,7 +93,7 @@ class HttpsRequestServiceAgent extends https.Agent { } }); return socket; - }; + } @bindThis private isPrivateIp(ip: string): boolean { From 6027b516e1c82324d55d6e54d0e17cbd816feb42 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 21:24:35 -0500 Subject: [PATCH 056/154] Fix `.punyHost` misuse --- packages/backend/src/core/RemoteUserResolveService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 678da0cfa6..098b5e1706 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -54,7 +54,7 @@ export class RemoteUserResolveService { }) as MiLocalUser; } - host = this.utilityService.punyHost(host); + host = this.utilityService.toPuny(host); if (host === this.utilityService.toPuny(this.config.host)) { this.logger.info(`return local user: ${usernameLower}`); From 59e160147fd38d49ebad27e0d705e3f2bf2613b4 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 Nov 2024 21:32:12 -0500 Subject: [PATCH 057/154] Bump develop version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 76cfbadb23..6e84882440 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.9.3", + "version": "2024.10.0-dev", "codename": "shonk", "repository": { "type": "git", From 4e0f7ced842b538519c64769cd4a5adf010203ca Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 14 Nov 2024 18:10:14 -0500 Subject: [PATCH 058/154] preserve the raw URI in parseUri --- packages/backend/src/core/activitypub/ApDbResolverService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index dd89716d34..f6b50ec704 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -66,9 +66,10 @@ export class ApDbResolverService implements OnApplicationShutdown { public parseUri(value: string | IObject | [string | IObject]): UriParseResult { const separator = '/'; - const uri = new URL(getApId(value)); + const apId = getApId(value); + const uri = new URL(apId); if (this.utilityService.toPuny(uri.host) !== this.utilityService.toPuny(this.config.host)) { - return { local: false, uri: uri.href }; + return { local: false, uri: apId }; } const [, type, id, ...rest] = uri.pathname.split(separator); From aabb1945e8a65af65437c8616b533491055e3c0d Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:54:47 -0500 Subject: [PATCH 059/154] respect pinned note limit for remote users --- .../backend/src/core/activitypub/models/ApPersonService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 2119c41569..6f8f3eca72 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -731,9 +731,10 @@ export class ApPersonService implements OnModuleInit { // Resolve and regist Notes const limit = promiseLimit(2); + const maxPinned = (await this.roleService.getUserPolicies(user.id)).pinLimit; const featuredNotes = await Promise.all(items .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも - .slice(0, 5) + .slice(0, maxPinned) .map(item => limit(() => this.apNoteService.resolveNote(item, { resolver: _resolver, sentFrom: new URL(user.uri), From 984cfe358d3558f75040ab2f7a043a454f6df84c Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 14 Nov 2024 18:47:28 -0500 Subject: [PATCH 060/154] reduce log spam from `updateFeatured` --- .../core/activitypub/models/ApPersonService.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 2119c41569..d2f5b64119 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; +import { AbortError } from 'node-fetch'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -482,7 +483,13 @@ export class ApPersonService implements OnModuleInit { } //#endregion - await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)); + await this.updateFeatured(user.id, resolver).catch(err => { + if (err instanceof AbortError || (err instanceof StatusError && err.isRetryable)) { + this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`); + } else { + this.logger.error('Failed to update featured notes:', err); + } + }); return user; } @@ -647,7 +654,13 @@ export class ApPersonService implements OnModuleInit { { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox }, ); - await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); + await this.updateFeatured(exist.id, resolver).catch(err => { + if (err instanceof AbortError || (err instanceof StatusError && err.isRetryable)) { + this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`); + } else { + this.logger.error('Failed to update featured notes:', err); + } + }); const updated = { ...exist, ...updates }; From fedf0d7e20e615485b79b393e597ed2619577df0 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 14 Nov 2024 20:32:59 -0500 Subject: [PATCH 061/154] further reduce log spam from `updateFeatured` errors --- .../activitypub/models/ApPersonService.ts | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index d2f5b64119..6754c6c41b 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -483,13 +483,7 @@ export class ApPersonService implements OnModuleInit { } //#endregion - await this.updateFeatured(user.id, resolver).catch(err => { - if (err instanceof AbortError || (err instanceof StatusError && err.isRetryable)) { - this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`); - } else { - this.logger.error('Failed to update featured notes:', err); - } - }); + await this.updateFeatured(user.id, resolver).catch(err => console.error(err)); return user; } @@ -654,13 +648,7 @@ export class ApPersonService implements OnModuleInit { { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox }, ); - await this.updateFeatured(exist.id, resolver).catch(err => { - if (err instanceof AbortError || (err instanceof StatusError && err.isRetryable)) { - this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`); - } else { - this.logger.error('Failed to update featured notes:', err); - } - }); + await this.updateFeatured(exist.id, resolver).catch(err => console.error(err)); const updated = { ...exist, ...updates }; @@ -735,7 +723,15 @@ export class ApPersonService implements OnModuleInit { const _resolver = resolver ?? this.apResolverService.createResolver(); // Resolve to (Ordered)Collection Object - const collection = await _resolver.resolveCollection(user.featured); + const collection = await _resolver.resolveCollection(user.featured).catch(err => { + if (err instanceof AbortError || err instanceof StatusError) { + this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`); + } else { + this.logger.error('Failed to update featured notes:', err); + } + }); + if (!collection) return; + if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); // Resolve to Object(may be Note) arrays From dcd5b6d972809bf5d4f6f0ae3f1d7d1e9edab54b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:01:59 -0500 Subject: [PATCH 062/154] replace `console.error` with `this.logger.error` (merge error) --- .../backend/src/core/activitypub/models/ApPersonService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 6754c6c41b..b03d6b1853 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -483,7 +483,7 @@ export class ApPersonService implements OnModuleInit { } //#endregion - await this.updateFeatured(user.id, resolver).catch(err => console.error(err)); + await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)); return user; } @@ -648,7 +648,7 @@ export class ApPersonService implements OnModuleInit { { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox }, ); - await this.updateFeatured(exist.id, resolver).catch(err => console.error(err)); + await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); const updated = { ...exist, ...updates }; From a62e4f1cf2c99150d345917c290f13e5a48ab92e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 14 Nov 2024 19:32:08 -0500 Subject: [PATCH 063/154] ignore `isNSFW` for pure renotes --- packages/backend/src/core/NoteCreateService.ts | 9 ++++++++- packages/backend/src/core/NoteEditService.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 1bc4599a60..ccc5bc0214 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -146,6 +146,8 @@ type Option = { app?: MiApp | null; }; +type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] }); + @Injectable() export class NoteCreateService implements OnApplicationShutdown { #shutdownController = new AbortController(); @@ -412,7 +414,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (user.host && !data.cw) { await this.federatedInstanceService.fetch(user.host).then(async i => { - if (i.isNSFW) { + if (i.isNSFW && !this.isPureRenote(data)) { data.cw = 'Instance is marked as NSFW'; } }); @@ -821,6 +823,11 @@ export class NoteCreateService implements OnApplicationShutdown { if (!user.noindex) this.index(note); } + @bindThis + private isPureRenote(note: Option): note is PureRenoteOption { + return this.isRenote(note) && !this.isQuote(note); + } + @bindThis private isRenote(note: Option): note is Option & { renote: MiNote } { return note.renote != null; diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index d31958e5d4..6c456fb4a3 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -142,6 +142,8 @@ type Option = { editcount?: boolean | null; }; +type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] }); + @Injectable() export class NoteEditService implements OnApplicationShutdown { #shutdownController = new AbortController(); @@ -442,7 +444,7 @@ export class NoteEditService implements OnApplicationShutdown { if (user.host && !data.cw) { await this.federatedInstanceService.fetch(user.host).then(async i => { - if (i.isNSFW) { + if (i.isNSFW && !this.isPureRenote(data)) { data.cw = 'Instance is marked as NSFW'; } }); @@ -787,6 +789,11 @@ export class NoteEditService implements OnApplicationShutdown { if (!user.noindex) this.index(note); } + @bindThis + private isPureRenote(note: Option): note is PureRenoteOption { + return this.isRenote(note) && !this.isQuote(note); + } + @bindThis private isRenote(note: Option): note is Option & { renote: MiNote } { return note.renote != null; From eb1e32681345aaa6184a540c7dba9ae4fd5a6e77 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 14 Nov 2024 19:50:34 -0500 Subject: [PATCH 064/154] add script to fix hellspawns --- UPGRADE_NOTES.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md index 8bebd4eb34..fb81a611b7 100644 --- a/UPGRADE_NOTES.md +++ b/UPGRADE_NOTES.md @@ -1,5 +1,38 @@ # Upgrade Notes +## 2024.10.0 + +### Hellspawns + +Sharkey versions before 2024.10 suffered from a bug in the "Mark instance as NSFW" feature. +When a user from such an instance boosted a note, the boost would be converted to a hellspawn (pure renote with Content Warning). +Hellspawns are buggy and do not properly federate, so it may be desirable to correct any that already exist in the database. +The following script will correct any local or remote hellspawns in the database. + +```postgresql +/* Remove "instance is marked as NSFW" hellspawns */ +UPDATE note +SET cw = null +WHERE + "renoteId" IS NOT NULL + AND "text" IS NULL + AND cw = 'Instance is marked as NSFW' + AND "replyId" IS NULL + AND "hasPoll" = false + AND "fileIds" = '{}'; + +/* Fix legacy / user-created hellspawns */ +UPDATE note +SET text = '.' +WHERE + "renoteId" IS NOT NULL + AND "text" IS NULL + AND cw IS NOT NULL + AND "replyId" IS NULL + AND "hasPoll" = false + AND "fileIds" = '{}'; +``` + ## 2024.9.0 ### Following Feed From c9934c379fcaaf02511f7d3ba63be44306feb722 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:31:17 -0500 Subject: [PATCH 065/154] remove duplicate `isPureRenote` method --- packages/backend/src/core/NoteCreateService.ts | 4 ++-- packages/backend/src/core/NoteEditService.ts | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index ccc5bc0214..892a929c41 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -146,7 +146,7 @@ type Option = { app?: MiApp | null; }; -type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] }); +export type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] }); @Injectable() export class NoteCreateService implements OnApplicationShutdown { @@ -824,7 +824,7 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private isPureRenote(note: Option): note is PureRenoteOption { + public isPureRenote(note: Option): note is PureRenoteOption { return this.isRenote(note) && !this.isQuote(note); } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 6c456fb4a3..e5e3c38cd3 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -142,8 +142,6 @@ type Option = { editcount?: boolean | null; }; -type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] }); - @Injectable() export class NoteEditService implements OnApplicationShutdown { #shutdownController = new AbortController(); @@ -444,7 +442,7 @@ export class NoteEditService implements OnApplicationShutdown { if (user.host && !data.cw) { await this.federatedInstanceService.fetch(user.host).then(async i => { - if (i.isNSFW && !this.isPureRenote(data)) { + if (i.isNSFW && !this.noteCreateService.isPureRenote(data)) { data.cw = 'Instance is marked as NSFW'; } }); @@ -789,11 +787,6 @@ export class NoteEditService implements OnApplicationShutdown { if (!user.noindex) this.index(note); } - @bindThis - private isPureRenote(note: Option): note is PureRenoteOption { - return this.isRenote(note) && !this.isQuote(note); - } - @bindThis private isRenote(note: Option): note is Option & { renote: MiNote } { return note.renote != null; From cc394d9a4bd560097ddfa7c4d11f7abe9a993fa8 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:32:13 -0500 Subject: [PATCH 066/154] quote all symbols in hellspawn upgrade script --- UPGRADE_NOTES.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md index fb81a611b7..c941de6643 100644 --- a/UPGRADE_NOTES.md +++ b/UPGRADE_NOTES.md @@ -11,23 +11,23 @@ The following script will correct any local or remote hellspawns in the database ```postgresql /* Remove "instance is marked as NSFW" hellspawns */ -UPDATE note -SET cw = null +UPDATE "note" +SET "cw" = null WHERE "renoteId" IS NOT NULL AND "text" IS NULL - AND cw = 'Instance is marked as NSFW' + AND "cw" = 'Instance is marked as NSFW' AND "replyId" IS NULL AND "hasPoll" = false AND "fileIds" = '{}'; /* Fix legacy / user-created hellspawns */ -UPDATE note -SET text = '.' +UPDATE "note" +SET "text" = '.' WHERE "renoteId" IS NOT NULL AND "text" IS NULL - AND cw IS NOT NULL + AND "cw" IS NOT NULL AND "replyId" IS NULL AND "hasPoll" = false AND "fileIds" = '{}'; From 0f6d26e065731f70b75afda3542381409cbc9569 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 14 Nov 2024 20:25:48 -0500 Subject: [PATCH 067/154] reduce log spam from charts --- packages/backend/src/core/chart/ChartManagementService.ts | 7 +++++++ packages/backend/src/core/chart/core.ts | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 79681370a1..316feec6ee 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -6,6 +6,8 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js'; +import Logger from '@/logger.js'; import FederationChart from './charts/federation.js'; import NotesChart from './charts/notes.js'; import UsersChart from './charts/users.js'; @@ -24,6 +26,7 @@ import type { OnApplicationShutdown } from '@nestjs/common'; export class ChartManagementService implements OnApplicationShutdown { private charts; private saveIntervalId: NodeJS.Timeout; + private readonly logger: Logger; constructor( private federationChart: FederationChart, @@ -38,6 +41,7 @@ export class ChartManagementService implements OnApplicationShutdown { private perUserFollowingChart: PerUserFollowingChart, private perUserDriveChart: PerUserDriveChart, private apRequestChart: ApRequestChart, + private chartLoggerService: ChartLoggerService, ) { this.charts = [ this.federationChart, @@ -53,6 +57,7 @@ export class ChartManagementService implements OnApplicationShutdown { this.perUserDriveChart, this.apRequestChart, ]; + this.logger = chartLoggerService.logger; } @bindThis @@ -62,6 +67,7 @@ export class ChartManagementService implements OnApplicationShutdown { for (const chart of this.charts) { chart.save(); } + this.logger.info('All charts saved'); }, 1000 * 60 * 20); } @@ -72,6 +78,7 @@ export class ChartManagementService implements OnApplicationShutdown { await Promise.all( this.charts.map(chart => chart.save()), ); + this.logger.info('All charts saved'); } } diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index af5485a46e..234c1d63b4 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -368,7 +368,7 @@ export default abstract class Chart { // 初期ログデータを作成 data = this.getNewLog(null); - this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`); + this.logger.debug(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`); } const date = Chart.dateToTimestamp(current); @@ -398,7 +398,7 @@ export default abstract class Chart { ...columns, }) as RawRecord; - this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); + this.logger.debug(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); return log; } finally { @@ -418,7 +418,7 @@ export default abstract class Chart { @bindThis public async save(): Promise { if (this.buffer.length === 0) { - this.logger.info(`${this.name}: Write skipped`); + this.logger.debug(`${this.name}: Write skipped`); return; } @@ -519,7 +519,7 @@ export default abstract class Chart { .execute(), ]); - this.logger.info(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`); + this.logger.debug(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`); // TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group)); From dff465000cfe10c8196ac6933e831eed5c7d687e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 16:49:58 -0500 Subject: [PATCH 068/154] fix import-order in ApInboxService --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 90444a1af3..45b3da5ca0 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -30,6 +30,7 @@ import type { MiRemoteUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { fromTuple } from '@/misc/from-tuple.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'; @@ -40,7 +41,6 @@ import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; -import { fromTuple } from '@/misc/from-tuple.js'; @Injectable() export class ApInboxService { From 8f42e8434eaebe3aba5d1980c57f49dd8ad0de91 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 16:50:43 -0500 Subject: [PATCH 069/154] fix exception handling for Like activities --- .../backend/src/core/activitypub/ApInboxService.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 45b3da5ca0..550dabf4de 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -31,6 +31,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { fromTuple } from '@/misc/from-tuple.js'; +import { IdentifiableError } from '@/misc/identifiable-error.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'; @@ -204,13 +205,16 @@ export class ApInboxService { await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null); - return await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name).catch(err => { - if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { + try { + await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name); + return 'ok'; + } catch (err) { + if (err instanceof IdentifiableError && err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { return 'skip: already reacted'; } else { throw err; } - }).then(() => 'ok'); + } } @bindThis From cfc3ab4b045af0674122fa49176431860176358b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 16:50:54 -0500 Subject: [PATCH 070/154] fix exception handling for Announce activities --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 550dabf4de..ea01086368 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -297,7 +297,7 @@ export class ApInboxService { const target = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); - return e; + throw e; }); if (isPost(target)) return await this.announceNote(actor, activity, target); From 0de7a084a99860b6f88bc2ce940dd6cb6d3543d5 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 16:51:12 -0500 Subject: [PATCH 071/154] fix exception handling for Undo activities --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index ea01086368..97c8ef3a9d 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -666,7 +666,7 @@ export class ApInboxService { const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); - return e; + throw e; }); // don't queue because the sender may attempt again when timeout From bcc20d6dc48590e60d63e8071c83986393470c1b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 16:26:00 -0500 Subject: [PATCH 072/154] allow Update activities for non-note posts --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 90444a1af3..dab8d98c23 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -803,7 +803,7 @@ export class ApInboxService { } else if (getApType(object) === 'Question') { await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); return 'ok: Question updated'; - } else if (getApType(object) === 'Note') { + } else if (isPost(object)) { await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); return 'ok: Note updated'; } else { From ca94959fff3f202c90166db8868a7613c424fe0b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 1 Nov 2024 16:52:31 -0400 Subject: [PATCH 073/154] factor out Following Feed list into SkFollowingRecentNotes.vue --- .../src/components/SkFollowingRecentNotes.vue | 122 +++++++++++++++ .../frontend/src/components/global/SkLazy.vue | 57 +++++++ .../frontend/src/pages/following-feed.vue | 144 +++--------------- 3 files changed, 202 insertions(+), 121 deletions(-) create mode 100644 packages/frontend/src/components/SkFollowingRecentNotes.vue create mode 100644 packages/frontend/src/components/global/SkLazy.vue diff --git a/packages/frontend/src/components/SkFollowingRecentNotes.vue b/packages/frontend/src/components/SkFollowingRecentNotes.vue new file mode 100644 index 0000000000..35fa83812f --- /dev/null +++ b/packages/frontend/src/components/SkFollowingRecentNotes.vue @@ -0,0 +1,122 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/SkLazy.vue b/packages/frontend/src/components/global/SkLazy.vue new file mode 100644 index 0000000000..40add97db7 --- /dev/null +++ b/packages/frontend/src/components/global/SkLazy.vue @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index d4bc295c78..a8d6a21795 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -12,55 +12,34 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - - - - - +
-
+ -
+ + + From 38e30c0d54ee1f73e79f7722473db51517cfe5d1 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 11:20:54 -0400 Subject: [PATCH 082/154] allow following-feed-utils to use alternate state backends --- .../src/scripts/following-feed-utils.ts | 100 +++++++++++++----- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts index 064d6b72e3..bf4266f830 100644 --- a/packages/frontend/src/scripts/following-feed-utils.ts +++ b/packages/frontend/src/scripts/following-feed-utils.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed } from 'vue'; +import { computed, Ref, WritableComputedRef } from 'vue'; import { defaultStore } from '@/store.js'; import { deepMerge } from '@/scripts/merge.js'; import { PageHeaderItem } from '@/types/page-header.js'; @@ -13,9 +13,45 @@ import { popupMenu } from '@/os.js'; export const followingTab = 'following' as const; export const mutualsTab = 'mutuals' as const; export const followersTab = 'followers' as const; -export type FollowingFeedTab = typeof followingTab | typeof mutualsTab | typeof followersTab; +export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const; +export type FollowingFeedTab = typeof followingFeedTabs[number]; -export function createOptions(): PageHeaderItem { +export function followingTabName(tab: FollowingFeedTab): string; +export function followingTabName(tab: FollowingFeedTab | null | undefined): null; +export function followingTabName(tab: FollowingFeedTab | null | undefined): string | null { + if (tab === followingTab) return i18n.ts.following; + if (tab === followersTab) return i18n.ts.followers; + if (tab === mutualsTab) return i18n.ts.mutuals; + return null; +} + +export function followingTabIcon(tab: FollowingFeedTab | null | undefined): string { + if (tab === followersTab) return 'ph-user ph-bold ph-lg'; + if (tab === mutualsTab) return 'ph-user-switch ph-bold ph-lg'; + return 'ph-user-check ph-bold ph-lg'; +} + +export type FollowingFeedModel = { + [Key in keyof FollowingFeedState]: WritableComputedRef; +} + +export interface FollowingFeedState { + withNonPublic: boolean, + withQuotes: boolean, + withBots: boolean, + withReplies: boolean, + onlyFiles: boolean, + userList: FollowingFeedTab, + remoteWarningDismissed: boolean, +} + +interface StorageInterface = Partial> { + readonly state: Partial; + readonly reactiveState: Ref>; + save(updated: T): void; +} + +export function createOptions(storage?: Ref): PageHeaderItem { const { userList, withNonPublic, @@ -23,7 +59,7 @@ export function createOptions(): PageHeaderItem { withBots, withReplies, onlyFiles, - } = createModel(); + } = createModel(storage); return { icon: 'ti ti-dots', @@ -62,41 +98,47 @@ export function createOptions(): PageHeaderItem { disabled: withReplies, }, ], ev.currentTarget ?? ev.target), - }; -} -export function createModel() { - const userList = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.userList, +export function createModel(storage?: Ref): FollowingFeedModel { + // eslint-disable-next-line no-param-reassign + storage ??= createDefaultStorage(); + + // Based on timeline.saveTlFilter() + const saveFollowingFilter = (key: K, value: FollowingFeedState[K]) => { + const state = deepMerge(storage.value.state, defaultFollowingFeedState); + const out = deepMerge({ [key]: value }, state); + storage.value.save(out); + }; + + const userList: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.userList ?? defaultFollowingFeedState.userList, set: value => saveFollowingFilter('userList', value), }); - - const withNonPublic = computed({ + const withNonPublic: WritableComputedRef = computed({ get: () => { if (userList.value === 'followers') return false; - return defaultStore.reactiveState.followingFeed.value.withNonPublic; + return storage.value.reactiveState.value.withNonPublic ?? defaultFollowingFeedState.withNonPublic; }, set: value => saveFollowingFilter('withNonPublic', value), }); - const withQuotes = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withQuotes, + const withQuotes: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withQuotes ?? defaultFollowingFeedState.withQuotes, set: value => saveFollowingFilter('withQuotes', value), }); - const withBots = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withBots, + const withBots: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withBots ?? defaultFollowingFeedState.withBots, set: value => saveFollowingFilter('withBots', value), }); - const withReplies = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withReplies, + const withReplies: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withReplies ?? defaultFollowingFeedState.withReplies, set: value => saveFollowingFilter('withReplies', value), }); - const onlyFiles = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles, + const onlyFiles: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles, set: value => saveFollowingFilter('onlyFiles', value), }); - - const remoteWarningDismissed = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.remoteWarningDismissed, + const remoteWarningDismissed: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed, set: value => saveFollowingFilter('remoteWarningDismissed', value), }); @@ -111,8 +153,12 @@ export function createModel() { }; } -// Based on timeline.saveTlFilter() -function saveFollowingFilter(key: Key, value: (typeof defaultStore.state.followingFeed)[Key]) { - const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed); - return defaultStore.set('followingFeed', out); +function createDefaultStorage() { + return computed(() => ({ + state: defaultStore.state.followingFeed, + reactiveState: defaultStore.reactiveState.followingFeed, + save(updated: typeof defaultStore.state.followingFeed) { + return defaultStore.set('followingFeed', updated); + }, + })); } From 1ca350e45dc81990a71afb1772b2230f17a3ba96 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 11:23:15 -0400 Subject: [PATCH 083/154] define defult Following Feed state in following-feed-utils.ts instead of store.ts --- .../src/scripts/following-feed-utils.ts | 21 ++++++++++++++++++- packages/frontend/src/store.ts | 12 ++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts index bf4266f830..3b4020f84b 100644 --- a/packages/frontend/src/scripts/following-feed-utils.ts +++ b/packages/frontend/src/scripts/following-feed-utils.ts @@ -45,13 +45,32 @@ export interface FollowingFeedState { remoteWarningDismissed: boolean, } +export const defaultFollowingFeedState: FollowingFeedState = { + withNonPublic: false, + withQuotes: false, + withBots: true, + withReplies: false, + onlyFiles: false, + userList: followingTab, + remoteWarningDismissed: false, +}; + interface StorageInterface = Partial> { readonly state: Partial; readonly reactiveState: Ref>; save(updated: T): void; } -export function createOptions(storage?: Ref): PageHeaderItem { +export function createHeaderItem(storage?: Ref): PageHeaderItem { + const menu = createOptionsMenu(storage); + return { + icon: 'ti ti-dots', + text: i18n.ts.options, + handler: ev => popupMenu(menu, ev.currentTarget ?? ev.target), + }; +} + +export function createOptionsMenu(storage?: Ref): MenuItem[] { const { userList, withNonPublic, diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index fd84ad2e4d..bbd9873ad8 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -11,7 +11,7 @@ import darkTheme from '@@/themes/d-ice.json5'; import { miLocalStorage } from './local-storage.js'; import { searchEngineMap } from './scripts/search-engine-map.js'; import type { SoundType } from '@/scripts/sound.js'; -import type { FollowingFeedTab } from '@/scripts/following-feed-utils.js'; +import { defaultFollowingFeedState } from '@/scripts/following-feed-utils.js'; import { Storage } from '@/pizzax.js'; interface PostFormAction { @@ -244,15 +244,7 @@ export const defaultStore = markRaw(new Storage('base', { }, followingFeed: { where: 'account', - default: { - withNonPublic: false, - withQuotes: false, - withBots: true, - withReplies: false, - onlyFiles: false, - userList: 'following' as FollowingFeedTab, - remoteWarningDismissed: false, - }, + default: defaultFollowingFeedState, }, overridedDeviceKind: { From 4a43e1a9e9c56eb0375714fffc147dfe080d5959 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 11:29:19 -0400 Subject: [PATCH 084/154] factor out remote followers warning in SkRemoteFollowersWarning.vue --- .../components/SkRemoteFollowersWarning.vue | 32 +++++++++++++++++++ .../frontend/src/pages/following-feed.vue | 10 +++--- 2 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 packages/frontend/src/components/SkRemoteFollowersWarning.vue diff --git a/packages/frontend/src/components/SkRemoteFollowersWarning.vue b/packages/frontend/src/components/SkRemoteFollowersWarning.vue new file mode 100644 index 0000000000..ceebbd59dd --- /dev/null +++ b/packages/frontend/src/components/SkRemoteFollowersWarning.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 33de5b01ea..9054769034 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.remoteFollowersWarning }} +
@@ -36,11 +36,12 @@ import { useRouter } from '@/router/supplier.js'; import MkPageHeader from '@/components/global/MkPageHeader.vue'; import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue'; import { useScrollPositionManager } from '@/nirax.js'; -import MkInfo from '@/components/MkInfo.vue'; import { createModel, createOptions, followersTab, followingTab, mutualsTab } from '@/scripts/following-feed-utils.js'; import SkLazy from '@/components/global/SkLazy.vue'; import SkFollowingRecentNotes from '@/components/SkFollowingRecentNotes.vue'; +import SkRemoteFollowersWarning from '@/components/SkRemoteFollowersWarning.vue'; +const model = createModel(); const { userList, withNonPublic, @@ -48,8 +49,7 @@ const { withBots, withReplies, onlyFiles, - remoteWarningDismissed, -} = createModel(); +} = model; const router = useRouter(); @@ -58,8 +58,6 @@ const followingRecentNotes = shallowRef>(); const noteScroll = shallowRef(); -const showRemoteWarning = computed(() => userList.value === 'followers' && !remoteWarningDismissed.value); - const selectedUserId: Ref = ref(null); function listReady(initialUserId?: string): void { From 2b0a62287542597f9f53f45cd932efeb8ec0a12e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 11:30:56 -0400 Subject: [PATCH 085/154] separate following feed's menu component from the actual filter options --- .../frontend/src/pages/following-feed.vue | 30 +++----- .../frontend/src/pages/user/recent-notes.vue | 4 +- .../src/scripts/following-feed-utils.ts | 72 +++++++++---------- 3 files changed, 46 insertions(+), 60 deletions(-) diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 9054769034..21e76b9094 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/user/recent-notes.vue b/packages/frontend/src/pages/user/recent-notes.vue index 6375979c00..d636068408 100644 --- a/packages/frontend/src/pages/user/recent-notes.vue +++ b/packages/frontend/src/pages/user/recent-notes.vue @@ -20,7 +20,7 @@ import { PageHeaderItem } from '@/types/page-header.js'; import MkPageHeader from '@/components/global/MkPageHeader.vue'; import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue'; import { acct } from '@/filters/user.js'; -import { createModel, createOptions } from '@/scripts/following-feed-utils.js'; +import { createModel, createHeaderItem } from '@/scripts/following-feed-utils.js'; import MkStickyContainer from '@/components/global/MkStickyContainer.vue'; defineProps<{ @@ -44,7 +44,7 @@ const headerActions: PageHeaderItem[] = [ text: i18n.ts.reload, handler: () => userRecentNotes.value?.reload(), }, - createOptions(), + createHeaderItem(), ]; // Based on user/index.vue diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts index 3b4020f84b..39f17949d6 100644 --- a/packages/frontend/src/scripts/following-feed-utils.ts +++ b/packages/frontend/src/scripts/following-feed-utils.ts @@ -9,6 +9,7 @@ import { deepMerge } from '@/scripts/merge.js'; import { PageHeaderItem } from '@/types/page-header.js'; import { i18n } from '@/i18n.js'; import { popupMenu } from '@/os.js'; +import { MenuItem } from '@/types/menu.js'; export const followingTab = 'following' as const; export const mutualsTab = 'mutuals' as const; @@ -80,43 +81,40 @@ export function createOptionsMenu(storage?: Ref): MenuItem[] { onlyFiles, } = createModel(storage); - return { - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: ev => - popupMenu([ - { - type: 'switch', - text: i18n.ts.showNonPublicNotes, - ref: withNonPublic, - disabled: userList.value === 'followers', - }, - { - type: 'switch', - text: i18n.ts.showQuotes, - ref: withQuotes, - }, - { - type: 'switch', - text: i18n.ts.showBots, - ref: withBots, - }, - { - type: 'switch', - text: i18n.ts.showReplies, - ref: withReplies, - disabled: onlyFiles, - }, - { - type: 'divider', - }, - { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: withReplies, - }, - ], ev.currentTarget ?? ev.target), + return [ + { + type: 'switch', + text: i18n.ts.showNonPublicNotes, + ref: withNonPublic, + disabled: computed(() => userList.value === followersTab), + }, + { + type: 'switch', + text: i18n.ts.showQuotes, + ref: withQuotes, + }, + { + type: 'switch', + text: i18n.ts.showBots, + ref: withBots, + }, + { + type: 'switch', + text: i18n.ts.showReplies, + ref: withReplies, + disabled: onlyFiles, + }, + { + type: 'divider', + }, + { + type: 'switch', + text: i18n.ts.fileAttachedOnly, + ref: onlyFiles, + disabled: withReplies, + }, + ]; +} export function createModel(storage?: Ref): FollowingFeedModel { // eslint-disable-next-line no-param-reassign From 83472dbd8240f44776561f56efc8dd2c69480f1b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 11:31:43 -0400 Subject: [PATCH 086/154] add following feed to the deck UI --- locales/index.d.ts | 8 ++ packages/frontend/src/ui/deck.vue | 2 + packages/frontend/src/ui/deck/deck-store.ts | 1 + .../frontend/src/ui/deck/following-column.vue | 124 ++++++++++++++++++ sharkey-locales/en-US.yml | 5 + 5 files changed, 140 insertions(+) create mode 100644 packages/frontend/src/ui/deck/following-column.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index d1cb1f97ea..b280ee33f8 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9661,6 +9661,10 @@ export interface Locale extends ILocale { * ロールタイムライン */ "roleTimeline": string; + /** + * Following + */ + "following": string; }; }; "_dialog": { @@ -11374,6 +11378,10 @@ export interface Locale extends ILocale { * Remote followers may have incomplete or outdated activity */ "remoteFollowersWarning": string; + /** + * Select a follow graph... + */ + "selectFollowingList": string; } declare const locales: { [lang: string]: Locale; diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 1e96b5d50e..b42a63e090 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -122,6 +122,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue'; import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; +import XFollowingColumn from '@/ui/deck/following-column.vue'; import { mainRouter } from '@/router/main.js'; import type { MenuItem } from '@/types/menu.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); @@ -138,6 +139,7 @@ const columnComponents = { mentions: XMentionsColumn, direct: XDirectColumn, roleTimeline: XRoleTimelineColumn, + following: XFollowingColumn, }; mainRouter.navHook = (path, flag): boolean => { diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 80f2c61f8c..ccc9af8d12 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -29,6 +29,7 @@ export const columnTypes = [ 'mentions', 'direct', 'roleTimeline', + 'following', ] as const; export type ColumnType = typeof columnTypes[number]; diff --git a/packages/frontend/src/ui/deck/following-column.vue b/packages/frontend/src/ui/deck/following-column.vue new file mode 100644 index 0000000000..b8fb432f3b --- /dev/null +++ b/packages/frontend/src/ui/deck/following-column.vue @@ -0,0 +1,124 @@ + + + + + + + + + + + diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 163fd0b0ae..342641d6d1 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -397,3 +397,8 @@ _auth: allowed: "Allowed" _announcement: new: "New" +_deck: + _columns: + following: "Following" + +selectFollowingList: "Select a follow graph..." From 5b48032681347af28cbb1c5bd94d5f55949b61aa Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 10:26:31 -0500 Subject: [PATCH 087/154] restore animation and styling in following-feed --- .../src/components/SkFollowingRecentNotes.vue | 24 ++++++++++++++++++- .../frontend/src/pages/following-feed.vue | 2 +- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/SkFollowingRecentNotes.vue b/packages/frontend/src/components/SkFollowingRecentNotes.vue index 35fa83812f..6daa8feba5 100644 --- a/packages/frontend/src/components/SkFollowingRecentNotes.vue +++ b/packages/frontend/src/components/SkFollowingRecentNotes.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -42,6 +42,7 @@ const props = defineProps<{ withReplies: boolean; withBots: boolean; onlyFiles: boolean; + selectedUserId?: string | null; }>(); const emit = defineEmits<{ @@ -119,4 +120,25 @@ function checkMute(note: Misskey.entities.Note | undefined | null, mutes: Mutes) .panel { background: var(--panel); } + +@keyframes border { + from { + border-left: 0 solid var(--accent); + } + to { + border-left: 6px solid var(--accent); + } +} + +.selected { + animation: border 0.2s ease-out 0s 1 forwards; + + &:first-child { + border-top-left-radius: 5px; + } + + &:last-child { + border-bottom-left-radius: 5px; + } +} diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 21e76b9094..91f74b2cf9 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
From c9afaba0d4416f2213430f47da8843257be6cfaf Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 10:31:04 -0500 Subject: [PATCH 088/154] adjust translation string "Select a follow relationship..." --- locales/index.d.ts | 4 ++-- packages/frontend/src/ui/deck/following-column.vue | 4 ++-- sharkey-locales/en-US.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index b280ee33f8..d8eb90f4e5 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11379,9 +11379,9 @@ export interface Locale extends ILocale { */ "remoteFollowersWarning": string; /** - * Select a follow graph... + * Select a follow relationship... */ - "selectFollowingList": string; + "selectFollowRelationship": string; } declare const locales: { [lang: string]: Locale; diff --git a/packages/frontend/src/ui/deck/following-column.vue b/packages/frontend/src/ui/deck/following-column.vue index b8fb432f3b..6b8c9db917 100644 --- a/packages/frontend/src/ui/deck/following-column.vue +++ b/packages/frontend/src/ui/deck/following-column.vue @@ -43,7 +43,7 @@ const columnIcon = computed(() => followingTabIcon(props.column.userList)); async function selectList(): Promise { const { canceled, result: newList } = await os.select({ - title: i18n.ts.selectFollowingList, + title: i18n.ts.selectFollowRelationship, items: followingFeedTabs.map(t => ({ value: t, text: followingTabName(t), @@ -97,7 +97,7 @@ const { const menu: MenuItem[] = [ { icon: columnIcon.value, - text: i18n.ts.selectFollowingList, + text: i18n.ts.selectFollowRelationship, action: selectList, }, ...createOptionsMenu(columnStorage), diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 342641d6d1..1f9b6ef4f5 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -401,4 +401,4 @@ _deck: _columns: following: "Following" -selectFollowingList: "Select a follow graph..." +selectFollowRelationship: "Select a follow relationship..." From c48faca707ca50c3f5ea05a4a6e1359b9fc7dcf6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 18 Nov 2024 10:33:32 -0500 Subject: [PATCH 089/154] fix lint errors in UrlPreviewService --- packages/backend/src/server/web/UrlPreviewService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 47cc09b067..adb188b66f 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { summaly } from '@misskey-dev/summaly'; import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -15,9 +16,8 @@ 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'; -import * as Redis from 'ioredis'; import { RedisKVCache } from '@/misc/cache.js'; +import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() export class UrlPreviewService { @@ -41,7 +41,7 @@ export class UrlPreviewService { this.previewCache = new RedisKVCache(this.redisClient, 'summaly', { lifetime: 1000 * 60 * 60 * 24, // 1d memoryCacheLifetime: 1000 * 60 * 10, // 10m - fetcher: (key: string) => { throw new Error('the UrlPreview cache should never fetch'); }, + fetcher: () => { throw new Error('the UrlPreview cache should never fetch'); }, toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), }); From 4c6cec552eb629f6c796bbc42db319e218f89515 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 18 Nov 2024 10:41:18 -0500 Subject: [PATCH 090/154] verify that preview URL is valid --- packages/backend/src/server/web/UrlPreviewService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index adb188b66f..26ea185586 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -65,7 +65,7 @@ export class UrlPreviewService { reply: FastifyReply, ): Promise { const url = request.query.url; - if (typeof url !== 'string') { + if (typeof url !== 'string' || !URL.canParse(url)) { reply.code(400); return; } From 2a4c432f41580cf2afa97bfd1a35153883b318db Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 18 Nov 2024 10:41:31 -0500 Subject: [PATCH 091/154] don't generate URL previews for blocked domains --- .../backend/src/server/web/UrlPreviewService.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 26ea185586..19dac1dfb8 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; import { MiMeta } from '@/models/Meta.js'; import { RedisKVCache } from '@/misc/cache.js'; +import { UtilityService } from '@/core/UtilityService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -36,6 +37,7 @@ export class UrlPreviewService { private httpRequestService: HttpRequestService, private loggerService: LoggerService, + private utilityService: UtilityService, ) { this.logger = this.loggerService.getLogger('url-preview'); this.previewCache = new RedisKVCache(this.redisClient, 'summaly', { @@ -87,6 +89,18 @@ export class UrlPreviewService { }; } + const host = new URL(url).host; + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { + reply.code(403); + return { + error: new ApiError({ + message: 'URL is blocked', + code: 'URL_PREVIEW_BLOCKED', + id: '50294652-857b-4b13-9700-8e5c7a8deae8', + }), + }; + } + const key = `${url}@${lang}`; const cached = await this.previewCache.get(key); if (cached !== undefined) { From 2fb2e523124119a13d045c95da98163b875a6f39 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 8 Nov 2024 09:55:26 -0500 Subject: [PATCH 092/154] add isPureRenotePacked --- packages/backend/src/misc/is-renote.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts index c128fded14..f9209f05b3 100644 --- a/packages/backend/src/misc/is-renote.ts +++ b/packages/backend/src/misc/is-renote.ts @@ -69,6 +69,14 @@ type PackedQuote = fileIds: NonNullable['fileIds']> }); +type PurePackedRenote = PackedRenote & { + text: NonNullable['text']>; + cw: NonNullable['cw']>; + replyId: NonNullable['replyId']>; + poll: NonNullable['poll']>; + fileIds: NonNullable['fileIds']>; +} + export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote { return note.renoteId != null; } @@ -80,3 +88,7 @@ export function isQuotePacked(note: PackedRenote): note is PackedQuote { note.poll != null || (note.fileIds != null && note.fileIds.length > 0); } + +export function isPureRenotePacked(note: Packed<'Note'>): note is PurePackedRenote { + return isRenotePacked(note) && !isQuotePacked(note); +} From faf1b3559a687853d2abd86136815c17b8ea79bc Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 8 Nov 2024 09:58:15 -0500 Subject: [PATCH 093/154] fix note hiding when renote and target have different visibility settings --- .../src/core/entities/NoteEntityService.ts | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 985245aeb1..0d0262d28e 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -16,6 +16,7 @@ import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { isPureRenotePacked } from '@/misc/is-renote.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CacheService } from '../CacheService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; @@ -122,29 +123,26 @@ export class NoteEntityService implements OnModuleInit { } else if (packedNote.renote && (meId === packedNote.renote.userId)) { hide = false; } else { - if (packedNote.renote) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: packedNote.renote.userId, - followerId: meId, - }, - }); + // フォロワーかどうか + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: packedNote.userId, + followerId: meId, + }, + }); - hide = !isFollowing; - } else { - // フォロワーかどうか - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: packedNote.userId, - followerId: meId, - }, - }); - - hide = !isFollowing; - } + hide = !isFollowing; } } + // If this is a pure renote (boost), then we should *also* check the boosted note's visibility. + // Otherwise we can have empty notes on the timeline, which is not good. + // Notes are packed in depth-first order, so we can safely grab the "isHidden" property to avoid duplicated checks. + // This is pulled out to ensure that we check both the renote *and* the boosted note. + if (packedNote.renote?.isHidden && isPureRenotePacked(packedNote)) { + hide = true; + } + if (!hide && meId && packedNote.userId !== meId) { const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(packedNote.userId); From 4b503f88e1ee882c3f80ce76f10f42592d646850 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:08:04 -0500 Subject: [PATCH 094/154] normalize naming of `isPackedPureRenote` and `PackedPureRenote` --- packages/backend/src/core/entities/NoteEntityService.ts | 4 ++-- packages/backend/src/misc/is-renote.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 0d0262d28e..0d3a0d15b9 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -16,7 +16,7 @@ import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; -import { isPureRenotePacked } from '@/misc/is-renote.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CacheService } from '../CacheService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; @@ -139,7 +139,7 @@ export class NoteEntityService implements OnModuleInit { // Otherwise we can have empty notes on the timeline, which is not good. // Notes are packed in depth-first order, so we can safely grab the "isHidden" property to avoid duplicated checks. // This is pulled out to ensure that we check both the renote *and* the boosted note. - if (packedNote.renote?.isHidden && isPureRenotePacked(packedNote)) { + if (packedNote.renote?.isHidden && isPackedPureRenote(packedNote)) { hide = true; } diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts index f9209f05b3..99c755e38f 100644 --- a/packages/backend/src/misc/is-renote.ts +++ b/packages/backend/src/misc/is-renote.ts @@ -69,7 +69,7 @@ type PackedQuote = fileIds: NonNullable['fileIds']> }); -type PurePackedRenote = PackedRenote & { +type PackedPureRenote = PackedRenote & { text: NonNullable['text']>; cw: NonNullable['cw']>; replyId: NonNullable['replyId']>; @@ -89,6 +89,6 @@ export function isQuotePacked(note: PackedRenote): note is PackedQuote { (note.fileIds != null && note.fileIds.length > 0); } -export function isPureRenotePacked(note: Packed<'Note'>): note is PurePackedRenote { +export function isPackedPureRenote(note: Packed<'Note'>): note is PackedPureRenote { return isRenotePacked(note) && !isQuotePacked(note); } From 47eb0daebbd95bd07469c1b0d82ffde6eda14978 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 16:41:47 -0500 Subject: [PATCH 095/154] fetch target note of Like(Note) activities --- packages/backend/src/core/activitypub/ApInboxService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 0b7ab7e19e..2f285a17cb 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -200,7 +200,10 @@ export class ApInboxService { private async like(actor: MiRemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); - const note = await this.apNoteService.fetchNote(targetUri); + const object = fromTuple(activity.object); + if (!object) return 'skip: activity has no object property'; + + const note = await this.apNoteService.resolveNote(object); if (!note) return `skip: target note not found ${targetUri}`; await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null); From 2bbccde2ce0f04afc7f2b20ba0c0404075cddef1 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 17:59:33 -0500 Subject: [PATCH 096/154] reduce inbox log spam when fetching blocked / unavailable notes --- .../backend/src/queue/QueueProcessorService.ts | 12 ++++++++---- .../src/queue/processors/InboxProcessorService.ts | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index eaeb6d58df..f130314e74 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -10,6 +10,7 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { StatusError } from '@/misc/status-error.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; @@ -132,7 +133,7 @@ export class QueueProcessorService implements OnApplicationShutdown { // 何故かeがundefinedで来ることがある if (!e) return '?'; - if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') { + if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError' || e instanceof StatusError) { return `${e.name}: ${e.message}`; } @@ -146,12 +147,15 @@ export class QueueProcessorService implements OnApplicationShutdown { function renderJob(job?: Bull.Job) { if (!job) return '?'; - return { - name: job.name || undefined, + const info: Record = { info: getJobInfo(job), - failedReason: job.failedReason || undefined, data: job.data, }; + + if (job.name) info.name = job.name; + if (job.failedReason) info.failedReason = job.failedReason; + + return info; } //#region system diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 102e835e24..260ebe0d40 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -7,6 +7,7 @@ import { URL } from 'node:url'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; +import { AbortError } from 'node-fetch'; import type Logger from '@/logger.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; @@ -232,6 +233,19 @@ export class InboxProcessorService implements OnApplicationShutdown { return e.message; } } + + if (e instanceof StatusError) { + if (e.isRetryable) { + return `temporary error ${e.statusCode}`; + } else { + return `skip: permanent error ${e.statusCode}`; + } + } + + if (e instanceof AbortError) { + return 'request aborted'; + } + throw e; } return 'ok'; From 9d3321fca4888253669b9e355636ae0cfdd9465b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 14 Nov 2024 14:54:46 -0500 Subject: [PATCH 097/154] allow Update(Note) and Update(Poll) to implicitly create missing notes --- .../src/core/activitypub/ApInboxService.ts | 18 ++++++++++++++---- .../core/activitypub/models/ApNoteService.ts | 9 +++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 2f285a17cb..57b30788bd 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -388,7 +388,7 @@ export class ApInboxService { } @bindThis - private async create(actor: MiRemoteUser, activity: ICreate, resolver?: Resolver): Promise { + private async create(actor: MiRemoteUser, activity: ICreate | IUpdate, resolver?: Resolver): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); @@ -423,14 +423,14 @@ export class ApInboxService { }); if (isPost(object)) { - await this.createNote(resolver, actor, object, false, activity); + await this.createNote(resolver, actor, object, false); } else { return `Unknown type: ${getApType(object)}`; } } @bindThis - private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { + private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false): Promise { const uri = getApId(note); if (typeof note === 'object') { @@ -789,7 +789,7 @@ export class ApInboxService { } @bindThis - private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise { + private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } @@ -808,9 +808,19 @@ export class ApInboxService { await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; } else if (getApType(object) === 'Question') { + // If we get an Update(Question) for a note that doesn't exist, then create it instead + if (!await this.apNoteService.hasNote(object)) { + return await this.create(actor, activity, resolver); + } + await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); return 'ok: Question updated'; } else if (isPost(object)) { + // If we get an Update(Note) for a note that doesn't exist, then create it instead + if (!await this.apNoteService.hasNote(object)) { + return await this.create(actor, activity, resolver); + } + await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); return 'ok: Note updated'; } else { diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index a0ddc2075b..a6d2ee1887 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -142,6 +142,15 @@ export class ApNoteService { return await this.apDbResolverService.getNoteFromApId(object); } + /** + * Returns true if the provided object / ID exists in the local database. + */ + @bindThis + public async hasNote(object: string | IObject | [string | IObject]): Promise { + const uri = getApId(object); + return await this.notesRepository.existsBy({ uri }); + } + /** * Noteを作成します。 */ From 9d5bc6cb287f071b035acdba47b35932b9b3490c Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 21 Nov 2024 10:29:29 -0500 Subject: [PATCH 098/154] pass resolver when creating notes via side-effect --- packages/backend/src/core/activitypub/ApInboxService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 57b30788bd..05c2de433a 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -165,7 +165,7 @@ export class ApInboxService { } else if (isAnnounce(activity)) { return await this.announce(actor, activity, resolver); } else if (isLike(activity)) { - return await this.like(actor, activity); + return await this.like(actor, activity, resolver); } else if (isUndo(activity)) { return await this.undo(actor, activity, resolver); } else if (isBlock(activity)) { @@ -197,13 +197,13 @@ export class ApInboxService { } @bindThis - private async like(actor: MiRemoteUser, activity: ILike): Promise { + private async like(actor: MiRemoteUser, activity: ILike, resolver?: Resolver): Promise { const targetUri = getApId(activity.object); const object = fromTuple(activity.object); if (!object) return 'skip: activity has no object property'; - const note = await this.apNoteService.resolveNote(object); + const note = await this.apNoteService.resolveNote(object, { resolver }); if (!note) return `skip: target note not found ${targetUri}`; await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null); From d74cf9e4ffdc688e298dc5f77bb7743f6c33cd11 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 15 Oct 2024 17:21:03 -0400 Subject: [PATCH 099/154] filter Add / Remove activities with non-Note payloads --- .../backend/src/core/activitypub/ApInboxService.ts | 14 +++++++++++--- packages/backend/src/core/activitypub/type.ts | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 0b7ab7e19e..4e4cdc09b5 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { fromTuple } from '@/misc/from-tuple.js'; import { IdentifiableError } from '@/misc/identifiable-error.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 { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isApObject, isNote, 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'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -271,8 +271,12 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const object = fromTuple(activity.object); - const note = await this.apNoteService.resolveNote(object, { resolver }); + const activityObject = fromTuple(activity.object); + if (isApObject(activityObject) && !isNote(activityObject)) { + return 'unsupported featured object type'; + } + + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.addPinned(actor, note.id); return; @@ -642,6 +646,10 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); + if (isApObject(activityObject) && !isNote(activityObject)) { + return 'unsupported featured object type'; + } + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.removePinned(actor, note.id); diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index af5aba9c16..08758aec80 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -340,6 +340,7 @@ export interface IMove extends IActivity { target: IObject | string; } +export const isApObject = (object: string | IObject): object is IObject => typeof(object) === 'object'; export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; From ae7b90de6cc86c42a1231a325cd55ef15e3d5b00 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 17:14:23 -0500 Subject: [PATCH 100/154] allow any valid post to be featured, not just Note --- packages/backend/src/core/activitypub/ApInboxService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 4e4cdc09b5..f90dec7a09 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -272,7 +272,7 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); - if (isApObject(activityObject) && !isNote(activityObject)) { + if (isApObject(activityObject) && !isPost(activityObject)) { return 'unsupported featured object type'; } @@ -646,7 +646,7 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); - if (isApObject(activityObject) && !isNote(activityObject)) { + if (isApObject(activityObject) && !isPost(activityObject)) { return 'unsupported featured object type'; } From 2b9c3f0d5ca5a7c577f10960f7bb571aff89bd37 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 21 Nov 2024 10:57:26 -0500 Subject: [PATCH 101/154] log type of unsupported featured object --- packages/backend/src/core/activitypub/ApInboxService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index f90dec7a09..26d51a8d9c 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -273,7 +273,7 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); if (isApObject(activityObject) && !isPost(activityObject)) { - return 'unsupported featured object type'; + return `unsupported featured object type: ${getApType(activityObject)}`; } const note = await this.apNoteService.resolveNote(activityObject, { resolver }); @@ -647,7 +647,7 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); if (isApObject(activityObject) && !isPost(activityObject)) { - return 'unsupported featured object type'; + return `unsupported featured object type: ${getApType(activityObject)}`; } const note = await this.apNoteService.resolveNote(activityObject, { resolver }); From e32fb4e86d832da5d8a2604453b327af3882e7e5 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 22 Nov 2024 09:22:26 -0500 Subject: [PATCH 102/154] remove unused import from ApInboxService.ts (introduced by merge error) --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 26d51a8d9c..b72bfb40b8 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { fromTuple } from '@/misc/from-tuple.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isApObject, isNote, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isApObject, 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'; import { ApDbResolverService } from './ApDbResolverService.js'; From 6b54405003df8984182b3b2b08295ae127d52b74 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 22 Nov 2024 13:53:41 -0500 Subject: [PATCH 103/154] add default / fallback rate limit --- packages/backend/src/server/api/ApiCallService.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 016db6ac19..6f51825494 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -311,7 +311,15 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError(accessDenied); } - if (ep.meta.limit) { + // For endpoints without a limit, the default is 10 calls per second + const endpointLimit: IEndpointMeta['limit'] = ep.meta.limit ?? { + duration: 1000, + max: 10, + }; + + // We don't need this check, but removing it would cause a big merge conflict. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (endpointLimit) { // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. let limitActor: string; if (user) { @@ -320,7 +328,7 @@ export class ApiCallService implements OnApplicationShutdown { limitActor = getIpHash(request.ip); } - const limit = Object.assign({}, ep.meta.limit); + const limit = Object.assign({}, endpointLimit); if (limit.key == null) { (limit as any).key = ep.name; From e3b826db5a2dd86c15b3c5f5bdfbd7fec8d781ad Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 22 Nov 2024 13:43:06 -0500 Subject: [PATCH 104/154] add rate limits to all public endpoints --- .../src/server/api/endpoints/announcements.ts | 6 + .../api/endpoints/announcements/show.ts | 6 + .../server/api/endpoints/antennas/create.ts | 6 + .../server/api/endpoints/antennas/delete.ts | 6 + .../src/server/api/endpoints/antennas/list.ts | 6 + .../server/api/endpoints/antennas/notes.ts | 6 + .../src/server/api/endpoints/antennas/show.ts | 6 + .../server/api/endpoints/antennas/update.ts | 6 + .../src/server/api/endpoints/app/create.ts | 6 + .../src/server/api/endpoints/app/show.ts | 6 + .../src/server/api/endpoints/auth/accept.ts | 6 + .../api/endpoints/auth/session/generate.ts | 6 + .../server/api/endpoints/auth/session/show.ts | 6 + .../api/endpoints/auth/session/userkey.ts | 6 + .../src/server/api/endpoints/blocking/list.ts | 6 + .../api/endpoints/bubble-game/ranking.ts | 6 + .../server/api/endpoints/channels/favorite.ts | 6 + .../server/api/endpoints/channels/featured.ts | 6 + .../server/api/endpoints/channels/follow.ts | 6 + .../server/api/endpoints/channels/followed.ts | 6 + .../api/endpoints/channels/my-favorites.ts | 6 + .../server/api/endpoints/channels/owned.ts | 6 + .../server/api/endpoints/channels/search.ts | 6 + .../src/server/api/endpoints/channels/show.ts | 6 + .../server/api/endpoints/channels/timeline.ts | 6 + .../api/endpoints/channels/unfavorite.ts | 6 + .../server/api/endpoints/channels/unfollow.ts | 6 + .../server/api/endpoints/channels/update.ts | 6 + .../api/endpoints/charts/active-users.ts | 6 + .../server/api/endpoints/charts/ap-request.ts | 6 + .../src/server/api/endpoints/charts/drive.ts | 6 + .../server/api/endpoints/charts/federation.ts | 6 + .../server/api/endpoints/charts/instance.ts | 6 + .../src/server/api/endpoints/charts/notes.ts | 6 + .../server/api/endpoints/charts/user/drive.ts | 6 + .../api/endpoints/charts/user/following.ts | 6 + .../server/api/endpoints/charts/user/notes.ts | 6 + .../server/api/endpoints/charts/user/pv.ts | 6 + .../api/endpoints/charts/user/reactions.ts | 6 + .../src/server/api/endpoints/charts/users.ts | 6 + .../server/api/endpoints/clips/add-note.ts | 3 +- .../src/server/api/endpoints/clips/create.ts | 6 + .../src/server/api/endpoints/clips/delete.ts | 6 + .../server/api/endpoints/clips/favorite.ts | 6 + .../src/server/api/endpoints/clips/list.ts | 6 + .../api/endpoints/clips/my-favorites.ts | 6 + .../src/server/api/endpoints/clips/notes.ts | 6 + .../server/api/endpoints/clips/remove-note.ts | 6 + .../src/server/api/endpoints/clips/show.ts | 6 + .../server/api/endpoints/clips/unfavorite.ts | 6 + .../src/server/api/endpoints/clips/update.ts | 6 + .../backend/src/server/api/endpoints/drive.ts | 6 + .../src/server/api/endpoints/drive/files.ts | 6 + .../endpoints/drive/files/attached-notes.ts | 6 + .../endpoints/drive/files/check-existence.ts | 6 + .../api/endpoints/drive/files/delete.ts | 7 + .../api/endpoints/drive/files/find-by-hash.ts | 6 + .../server/api/endpoints/drive/files/find.ts | 6 + .../server/api/endpoints/drive/files/show.ts | 6 + .../api/endpoints/drive/files/update.ts | 7 + .../src/server/api/endpoints/drive/folders.ts | 6 + .../api/endpoints/drive/folders/delete.ts | 7 + .../api/endpoints/drive/folders/find.ts | 6 + .../api/endpoints/drive/folders/show.ts | 6 + .../api/endpoints/drive/folders/update.ts | 7 + .../src/server/api/endpoints/drive/stream.ts | 6 + .../api/endpoints/email-address/available.ts | 6 + .../backend/src/server/api/endpoints/emoji.ts | 6 + .../src/server/api/endpoints/emojis.ts | 6 + .../src/server/api/endpoints/endpoint.ts | 6 + .../src/server/api/endpoints/endpoints.ts | 6 + .../api/endpoints/federation/followers.ts | 6 + .../api/endpoints/federation/following.ts | 6 + .../api/endpoints/federation/instances.ts | 6 + .../api/endpoints/federation/show-instance.ts | 6 + .../server/api/endpoints/federation/stats.ts | 6 + .../federation/update-remote-user.ts | 6 + .../server/api/endpoints/federation/users.ts | 6 + .../src/server/api/endpoints/fetch-rss.ts | 6 + .../src/server/api/endpoints/flash/delete.ts | 6 + .../server/api/endpoints/flash/featured.ts | 6 + .../src/server/api/endpoints/flash/like.ts | 6 + .../server/api/endpoints/flash/my-likes.ts | 6 + .../src/server/api/endpoints/flash/my.ts | 6 + .../src/server/api/endpoints/flash/show.ts | 6 + .../src/server/api/endpoints/flash/unlike.ts | 6 + .../endpoints/following/requests/accept.ts | 6 + .../endpoints/following/requests/cancel.ts | 6 + .../api/endpoints/following/requests/list.ts | 6 + .../endpoints/following/requests/reject.ts | 6 + .../api/endpoints/following/requests/sent.ts | 6 + .../server/api/endpoints/gallery/featured.ts | 6 + .../server/api/endpoints/gallery/popular.ts | 6 + .../src/server/api/endpoints/gallery/posts.ts | 6 + .../api/endpoints/gallery/posts/delete.ts | 6 + .../api/endpoints/gallery/posts/like.ts | 6 + .../api/endpoints/gallery/posts/show.ts | 6 + .../api/endpoints/gallery/posts/unlike.ts | 6 + .../api/endpoints/get-avatar-decorations.ts | 6 + .../api/endpoints/get-online-users-count.ts | 6 + .../src/server/api/endpoints/hashtags/list.ts | 6 + .../server/api/endpoints/hashtags/search.ts | 6 + .../src/server/api/endpoints/hashtags/show.ts | 6 + .../server/api/endpoints/hashtags/trend.ts | 6 + .../server/api/endpoints/hashtags/users.ts | 6 + .../backend/src/server/api/endpoints/i.ts | 6 + .../src/server/api/endpoints/i/2fa/done.ts | 7 + .../api/endpoints/i/2fa/password-less.ts | 7 + .../server/api/endpoints/i/2fa/update-key.ts | 7 + .../src/server/api/endpoints/i/apps.ts | 6 + .../server/api/endpoints/i/authorized-apps.ts | 6 + .../api/endpoints/i/claim-achievement.ts | 6 + .../src/server/api/endpoints/i/favorites.ts | 6 + .../server/api/endpoints/i/gallery/likes.ts | 6 + .../server/api/endpoints/i/gallery/posts.ts | 6 + .../src/server/api/endpoints/i/page-likes.ts | 6 + .../src/server/api/endpoints/i/pages.ts | 6 + .../backend/src/server/api/endpoints/i/pin.ts | 6 + .../api/endpoints/i/read-all-unread-notes.ts | 6 + .../api/endpoints/i/read-announcement.ts | 6 + .../api/endpoints/i/registry/get-all.ts | 6 + .../api/endpoints/i/registry/get-detail.ts | 6 + .../api/endpoints/i/registry/get-unsecure.ts | 6 + .../server/api/endpoints/i/registry/get.ts | 8 +- .../endpoints/i/registry/keys-with-type.ts | 6 + .../server/api/endpoints/i/registry/keys.ts | 6 + .../server/api/endpoints/i/registry/remove.ts | 6 + .../i/registry/scopes-with-domain.ts | 8 +- .../server/api/endpoints/i/registry/set.ts | 6 + .../server/api/endpoints/i/revoke-token.ts | 6 + .../server/api/endpoints/i/signin-history.ts | 6 + .../src/server/api/endpoints/i/unpin.ts | 6 + .../server/api/endpoints/i/webhooks/create.ts | 6 + .../server/api/endpoints/i/webhooks/delete.ts | 6 + .../server/api/endpoints/i/webhooks/list.ts | 6 + .../server/api/endpoints/i/webhooks/show.ts | 6 + .../server/api/endpoints/i/webhooks/update.ts | 5 + .../src/server/api/endpoints/invite/create.ts | 6 + .../src/server/api/endpoints/invite/delete.ts | 6 + .../src/server/api/endpoints/invite/limit.ts | 6 + .../src/server/api/endpoints/invite/list.ts | 6 + .../backend/src/server/api/endpoints/meta.ts | 6 + .../server/api/endpoints/miauth/gen-token.ts | 6 + .../src/server/api/endpoints/mute/delete.ts | 7 + .../src/server/api/endpoints/mute/list.ts | 6 + .../src/server/api/endpoints/my/apps.ts | 6 + .../backend/src/server/api/endpoints/notes.ts | 8 + .../api/endpoints/notes/bubble-timeline.ts | 6 + .../server/api/endpoints/notes/children.ts | 6 + .../src/server/api/endpoints/notes/clips.ts | 6 + .../api/endpoints/notes/conversation.ts | 6 + .../api/endpoints/notes/favorites/delete.ts | 7 + .../server/api/endpoints/notes/featured.ts | 6 + .../server/api/endpoints/notes/following.ts | 6 + .../api/endpoints/notes/global-timeline.ts | 8 +- .../api/endpoints/notes/hybrid-timeline.ts | 6 + .../src/server/api/endpoints/notes/like.ts | 6 + .../api/endpoints/notes/local-timeline.ts | 6 + .../server/api/endpoints/notes/mentions.ts | 6 + .../endpoints/notes/polls/recommendation.ts | 6 + .../api/endpoints/notes/polls/refresh.ts | 6 + .../server/api/endpoints/notes/polls/vote.ts | 6 + .../server/api/endpoints/notes/reactions.ts | 6 + .../api/endpoints/notes/reactions/create.ts | 6 + .../src/server/api/endpoints/notes/renotes.ts | 9 +- .../src/server/api/endpoints/notes/replies.ts | 6 + .../api/endpoints/notes/search-by-tag.ts | 6 + .../src/server/api/endpoints/notes/search.ts | 6 + .../src/server/api/endpoints/notes/show.ts | 10 +- .../src/server/api/endpoints/notes/state.ts | 6 + .../endpoints/notes/thread-muting/delete.ts | 6 + .../server/api/endpoints/notes/timeline.ts | 6 + .../server/api/endpoints/notes/translate.ts | 6 + .../api/endpoints/notes/user-list-timeline.ts | 6 + .../server/api/endpoints/notes/versions.ts | 10 +- .../api/endpoints/notifications/flush.ts | 6 + .../notifications/mark-all-as-read.ts | 6 + .../src/server/api/endpoints/page-push.ts | 6 + .../src/server/api/endpoints/pages/delete.ts | 7 + .../server/api/endpoints/pages/featured.ts | 6 + .../src/server/api/endpoints/pages/like.ts | 6 + .../src/server/api/endpoints/pages/show.ts | 6 + .../src/server/api/endpoints/pages/unlike.ts | 6 + .../backend/src/server/api/endpoints/ping.ts | 6 + .../src/server/api/endpoints/pinned-users.ts | 6 + .../src/server/api/endpoints/promo/read.ts | 6 + .../api/endpoints/renote-mute/delete.ts | 6 + .../server/api/endpoints/renote-mute/list.ts | 6 + .../src/server/api/endpoints/reset-db.ts | 6 + .../server/api/endpoints/reset-password.ts | 6 + .../src/server/api/endpoints/retention.ts | 6 + .../api/endpoints/reversi/cancel-match.ts | 6 + .../src/server/api/endpoints/reversi/games.ts | 6 + .../api/endpoints/reversi/invitations.ts | 6 + .../src/server/api/endpoints/reversi/match.ts | 6 + .../server/api/endpoints/reversi/show-game.ts | 6 + .../server/api/endpoints/reversi/surrender.ts | 6 + .../server/api/endpoints/reversi/verify.ts | 6 + .../src/server/api/endpoints/roles/list.ts | 6 + .../src/server/api/endpoints/roles/notes.ts | 6 + .../src/server/api/endpoints/roles/show.ts | 6 + .../src/server/api/endpoints/roles/users.ts | 6 + .../src/server/api/endpoints/server-info.ts | 6 + .../src/server/api/endpoints/sponsors.ts | 6 + .../backend/src/server/api/endpoints/stats.ts | 6 + .../src/server/api/endpoints/sw/register.ts | 6 + .../api/endpoints/sw/show-registration.ts | 6 + .../src/server/api/endpoints/sw/unregister.ts | 6 + .../api/endpoints/sw/update-registration.ts | 6 + .../backend/src/server/api/endpoints/test.ts | 6 + .../api/endpoints/username/available.ts | 6 + .../backend/src/server/api/endpoints/users.ts | 6 + .../api/endpoints/users/achievements.ts | 6 + .../src/server/api/endpoints/users/clips.ts | 6 + .../api/endpoints/users/featured-notes.ts | 6 + .../src/server/api/endpoints/users/flashs.ts | 6 + .../server/api/endpoints/users/followers.ts | 6 + .../server/api/endpoints/users/following.ts | 6 + .../api/endpoints/users/gallery/posts.ts | 6 + .../users/get-frequently-replied-users.ts | 6 + .../users/lists/create-from-public.ts | 6 + .../api/endpoints/users/lists/create.ts | 6 + .../api/endpoints/users/lists/delete.ts | 6 + .../api/endpoints/users/lists/favorite.ts | 6 + .../endpoints/users/lists/get-memberships.ts | 6 + .../server/api/endpoints/users/lists/list.ts | 6 + .../server/api/endpoints/users/lists/pull.ts | 6 + .../server/api/endpoints/users/lists/push.ts | 6 + .../server/api/endpoints/users/lists/show.ts | 6 + .../api/endpoints/users/lists/unfavorite.ts | 6 + .../users/lists/update-membership.ts | 6 + .../api/endpoints/users/lists/update.ts | 6 + .../src/server/api/endpoints/users/notes.ts | 6 + .../src/server/api/endpoints/users/pages.ts | 6 + .../server/api/endpoints/users/reactions.ts | 6 + .../api/endpoints/users/recommendation.ts | 6 + .../server/api/endpoints/users/relation.ts | 6 + .../api/endpoints/users/report-abuse.ts | 6 + .../users/search-by-username-and-host.ts | 6 + .../src/server/api/endpoints/users/search.ts | 6 + .../src/server/api/endpoints/users/show.ts | 6 + .../server/api/endpoints/users/update-memo.ts | 6 + packages/misskey-js/src/autogen/types.ts | 1440 +++++++++++++++++ 243 files changed, 2908 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index ff8dd73605..b2faf675b0 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Announcement', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/announcements/show.ts b/packages/backend/src/server/api/endpoints/announcements/show.ts index 6312a0a54c..20ae6cc2e5 100644 --- a/packages/backend/src/server/api/endpoints/announcements/show.ts +++ b/packages/backend/src/server/api/endpoints/announcements/show.ts @@ -27,6 +27,12 @@ export const meta = { id: 'b57b5e1d-4f49-404a-9edb-46b00268f121', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index e0c8ddcc84..9a6caa3317 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -47,6 +47,12 @@ export const meta = { optional: false, nullable: false, ref: 'Antenna', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index 2258954b56..70a9f819f5 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -24,6 +24,12 @@ export const meta = { id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts index 83d29f9c8c..ed7d1c9daa 100644 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ b/packages/backend/src/server/api/endpoints/antennas/list.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Antenna', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index f4dfe1ecc4..be4cf1e0ca 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -41,6 +41,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts index a40f187d0b..3ca20dfb3e 100644 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ b/packages/backend/src/server/api/endpoints/antennas/show.ts @@ -30,6 +30,12 @@ export const meta = { optional: false, nullable: false, ref: 'Antenna', }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 10f26b1912..919a4cb3f5 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -45,6 +45,12 @@ export const meta = { optional: false, nullable: false, ref: 'Antenna', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index ba847fc4f0..6d595289de 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -22,6 +22,12 @@ export const meta = { optional: false, nullable: false, ref: 'App', }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index 3db9a0d0d4..a8a3e79af1 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -26,6 +26,12 @@ export const meta = { optional: false, nullable: false, ref: 'App', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 2e62f04df0..0000ce16ef 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -26,6 +26,12 @@ export const meta = { id: '9c72d8de-391a-43c1-9d06-08d29efde8df', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { 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 f8ddfdb75c..a0ee1bfc73 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -40,6 +40,12 @@ export const meta = { id: '92f93e63-428e-4f2f-a5a4-39e1407fe998', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts index 13e02a2541..ba7ad04f37 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts @@ -43,6 +43,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts index b490c5832d..8e9aff8058 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts @@ -51,6 +51,12 @@ export const meta = { id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 8431fa6b34..ecbfb10d53 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Blocking', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts index ab877bbe20..5597570f8e 100644 --- a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts +++ b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts @@ -40,6 +40,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/favorite.ts b/packages/backend/src/server/api/endpoints/channels/favorite.ts index a1ae9b80a7..7ae5eb3437 100644 --- a/packages/backend/src/server/api/endpoints/channels/favorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/favorite.ts @@ -26,6 +26,12 @@ export const meta = { id: '4938f5f3-6167-4c04-9149-6607b7542861', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts index a9a79ba8fc..24323cbe63 100644 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ b/packages/backend/src/server/api/endpoints/channels/featured.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Channel', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 1812820ba2..5505f3ed24 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -26,6 +26,12 @@ export const meta = { id: 'c0031718-d573-4e85-928e-10039f1fbb68', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index d2f36f251e..e667b0b881 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Channel', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts index d96e6c3ad2..72a1cc0cf9 100644 --- a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Channel', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts index daab685f1b..6e51add6b2 100644 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ b/packages/backend/src/server/api/endpoints/channels/owned.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Channel', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index ae32203603..9476c494a3 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Channel', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts index 332ce2c9dc..e9c0c392c0 100644 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ b/packages/backend/src/server/api/endpoints/channels/show.ts @@ -28,6 +28,12 @@ export const meta = { id: '6f6c314b-7486-4897-8966-c04a66a02923', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 06130464a9..0bd01d712c 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -38,6 +38,12 @@ export const meta = { id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts index fc6b75e295..a5db833704 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts @@ -25,6 +25,12 @@ export const meta = { id: '353c68dd-131a-476c-aa99-88a345e83668', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index 48c5261135..aea34d9d47 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -26,6 +26,12 @@ export const meta = { id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index dba2938b39..d2a75225ed 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -10,6 +10,7 @@ import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import ms from 'ms'; export const meta = { tags: ['channels'], @@ -43,6 +44,11 @@ export const meta = { id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b', }, }, + + limit: { + duration: ms('1hour'), + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts index fd21e3d9fe..f6c0c045df 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts index cbe792376b..4c5c0d5d20 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index d32bc765a4..8210ec8fe7 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index dad21e9e8e..56a5dbea31 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index 68aa12ac0e..7f79e1356d 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index e1979cfe8b..b3660b558b 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index dcb72084b7..716c41f385 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 0a019ce4fb..b67b5ca338 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index 06b15bca18..e5587cab86 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/pv.ts b/packages/backend/src/server/api/endpoints/charts/user/pv.ts index d359b491e2..cbae3a21c1 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index 4355aa5348..d734240742 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index 1f5f5fea54..6e1a8ebd4f 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index d7c9ea3964..7816ea3ac9 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -18,9 +18,10 @@ export const meta = { kind: 'write:account', + // 60 calls per hour limit: { duration: ms('1hour'), - max: 20, + max: 60, }, errors: { diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index ceebc8ba5e..c2f72ad9ae 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -32,6 +32,12 @@ export const meta = { id: '920f7c2d-6208-4b76-8082-e632020f5883', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts index ca8ff2e1f1..f3f8705c44 100644 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ b/packages/backend/src/server/api/endpoints/clips/delete.ts @@ -22,6 +22,12 @@ export const meta = { id: '70ca08ba-6865-4630-b6fb-8494759aa754', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts index 11f8ec3e92..a6b97a6a80 100644 --- a/packages/backend/src/server/api/endpoints/clips/favorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts @@ -32,6 +32,12 @@ export const meta = { id: '92658936-c625-4273-8326-2d790129256e', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index 2e4a3ff820..cd4c6bb2fc 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Clip', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts index 44719592d1..1f9b24e6c9 100644 --- a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Clip', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 943c31c894..6175c4d0e5 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -35,6 +35,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 33f9ecd25b..2c503d4af5 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -30,6 +30,12 @@ export const meta = { id: 'aff017de-190e-434b-893e-33a9ff5049d8', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index 1078a1b176..949f0e70aa 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -30,6 +30,12 @@ export const meta = { optional: false, nullable: false, ref: 'Clip', }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts index a458fda4a0..e436e5b94e 100644 --- a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts @@ -31,6 +31,12 @@ export const meta = { id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 603a3ccf3d..b776f1357d 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -31,6 +31,12 @@ export const meta = { optional: false, nullable: false, ref: 'Clip', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index eb45e29f9e..a05372909c 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -29,6 +29,12 @@ export const meta = { }, }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index d615036ce8..8e821da0da 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'DriveFile', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index b86059b5e7..32c2620915 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -38,6 +38,12 @@ export const meta = { id: 'c118ece3-2e4b-4296-99d1-51756e32d232', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index cc7920505f..dfb83efc45 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -21,6 +21,12 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index fa6e11da49..7a009b12a1 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -11,6 +11,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['drive'], @@ -34,6 +35,12 @@ export const meta = { id: '5eb8d909-2540-4970-90b8-dd6f86088121', }, }, + + // 100 calls per minute + limit: { + duration: 1000 * 60, + max: 100, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index 090cff6875..e8fba144b5 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'DriveFile', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { 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 502d42f9e0..377af3db69 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'DriveFile', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index e8f4539d61..64f6a8b2ba 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -40,6 +40,12 @@ export const meta = { id: '25b73c73-68b1-41d0-bad1-381cfdf6579f', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 1501abf9ce..94a0e673a3 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -11,6 +11,7 @@ import { RoleService } from '@/core/RoleService.js'; import { DriveService } from '@/core/DriveService.js'; import type { Config } from '@/config.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['drive'], @@ -63,6 +64,12 @@ export const meta = { optional: false, nullable: false, ref: 'DriveFile', }, + + // 100 calls per minute + limit: { + duration: 1000 * 60, + max: 100, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index 9bcd824882..1245706b0d 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'DriveFolder', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index 85d63873a4..29be2121cd 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -9,6 +9,7 @@ import type { DriveFoldersRepository, DriveFilesRepository } from '@/models/_.js import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['drive'], @@ -30,6 +31,12 @@ export const meta = { id: 'b0fc8a17-963c-405d-bfbc-859a487295e1', }, }, + + // 100 calls per minute + limit: { + duration: 1000 * 60, + max: 100, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts index eb45a30bc0..950aeacea0 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'DriveFolder', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts index a1c0df6697..520d6b1b58 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -30,6 +30,12 @@ export const meta = { id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 62b04e1df3..cd47c0fc68 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -10,6 +10,7 @@ import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityServi import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['drive'], @@ -43,6 +44,12 @@ export const meta = { optional: false, nullable: false, ref: 'DriveFolder', }, + + // 100 calls per minute + limit: { + duration: 1000 * 60, + max: 100, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts index f7c1ed39b5..4cb0a91546 100644 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'DriveFile', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts index 1d7dacd60e..b4982fc8cf 100644 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ b/packages/backend/src/server/api/endpoints/email-address/available.ts @@ -26,6 +26,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/emoji.ts b/packages/backend/src/server/api/endpoints/emoji.ts index ccfbda0d44..45cc4a10f3 100644 --- a/packages/backend/src/server/api/endpoints/emoji.ts +++ b/packages/backend/src/server/api/endpoints/emoji.ts @@ -22,6 +22,12 @@ export const meta = { optional: false, nullable: false, ref: 'EmojiDetailed', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 46ef4eca1b..d5a14ca8f4 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -32,6 +32,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index fe7e9c36f3..7629cd7a67 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -28,6 +28,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 4aedf62a84..8878f5d570 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -26,6 +26,12 @@ export const meta = { '...', ], }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index d5b80035df..e7b528dda1 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -22,6 +22,12 @@ export const meta = { ref: 'Following', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 215f94fbcc..070a1c8407 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -22,6 +22,12 @@ export const meta = { ref: 'Following', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index c1ce3f2238..97384d2498 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'FederationInstance', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 2972861a4b..8168f9296f 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -20,6 +20,12 @@ export const meta = { optional: false, nullable: true, ref: 'FederationInstance', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index 69900bff9a..54d29c2faa 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -50,6 +50,12 @@ export const meta = { otherFollowingCount: { type: 'number' }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts index f8430ef431..3ec9522c44 100644 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts @@ -12,6 +12,12 @@ export const meta = { tags: ['federation'], requireCredential: false, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index 71b1aeb07b..e3bc2989df 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'UserDetailedNotMe', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index ba48b0119e..03f35f16a5 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -203,6 +203,12 @@ export const meta = { }, }, }, + + // 20 calls per 10 seconds + limit: { + duration: 1000 * 10, + max: 20, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts index 6912450abf..1010567113 100644 --- a/packages/backend/src/server/api/endpoints/flash/delete.ts +++ b/packages/backend/src/server/api/endpoints/flash/delete.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import ms from 'ms'; export const meta = { tags: ['flashs'], @@ -31,6 +32,11 @@ export const meta = { id: '1036ad7b-9f92-4fff-89c3-0e50dc941704', }, }, + + limit: { + duration: ms('1hour'), + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts index c2d6ab5085..2e8cbffe2a 100644 --- a/packages/backend/src/server/api/endpoints/flash/featured.ts +++ b/packages/backend/src/server/api/endpoints/flash/featured.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Flash', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts index e4dc5b61c5..378f9280f7 100644 --- a/packages/backend/src/server/api/endpoints/flash/like.ts +++ b/packages/backend/src/server/api/endpoints/flash/like.ts @@ -38,6 +38,12 @@ export const meta = { id: '010065cf-ad43-40df-8067-abff9f4686e3', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/my-likes.ts b/packages/backend/src/server/api/endpoints/flash/my-likes.ts index 755cc5acfc..22eae381da 100644 --- a/packages/backend/src/server/api/endpoints/flash/my-likes.ts +++ b/packages/backend/src/server/api/endpoints/flash/my-likes.ts @@ -36,6 +36,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/my.ts b/packages/backend/src/server/api/endpoints/flash/my.ts index 5746096232..48f464c337 100644 --- a/packages/backend/src/server/api/endpoints/flash/my.ts +++ b/packages/backend/src/server/api/endpoints/flash/my.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Flash', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/show.ts b/packages/backend/src/server/api/endpoints/flash/show.ts index a6fbd8e76e..03d9710773 100644 --- a/packages/backend/src/server/api/endpoints/flash/show.ts +++ b/packages/backend/src/server/api/endpoints/flash/show.ts @@ -28,6 +28,12 @@ export const meta = { id: 'f0d34a1a-d29a-401d-90ba-1982122b5630', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/unlike.ts b/packages/backend/src/server/api/endpoints/flash/unlike.ts index 7869bcdf52..6f45198e67 100644 --- a/packages/backend/src/server/api/endpoints/flash/unlike.ts +++ b/packages/backend/src/server/api/endpoints/flash/unlike.ts @@ -31,6 +31,12 @@ export const meta = { id: '755f25a7-9871-4f65-9f34-51eaad9ae0ac', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index 2d1446681c..4cf7413001 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -28,6 +28,12 @@ export const meta = { id: 'bcde4f8b-0913-4614-8881-614e522fb041', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 6d663d480c..9bcd726b12 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -37,6 +37,12 @@ export const meta = { optional: false, nullable: false, ref: 'UserLite', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { 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 fa59e38976..7ec217fdc4 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -42,6 +42,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index 4f78eae677..0c49b27aad 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -23,6 +23,12 @@ export const meta = { id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/sent.ts b/packages/backend/src/server/api/endpoints/following/requests/sent.ts index 6325f01bb8..7f99c228f4 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/sent.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/sent.ts @@ -42,6 +42,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index 7d2878e03f..abbfb9b83b 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts index 4ee252104a..71b979ab9f 100644 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index d398418ab4..630b6cede5 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts @@ -22,6 +22,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index b6b94db161..68478ba55c 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['gallery'], @@ -31,6 +32,11 @@ export const meta = { id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496', }, }, + + limit: { + duration: ms('1hour'), + max: 300, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 91e49e6463..e73110648c 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -39,6 +39,12 @@ export const meta = { id: '40e9ed56-a59c-473a-bf3f-f289c54fb5a7', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts index bd69898229..fd637febaa 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts @@ -28,6 +28,12 @@ export const meta = { optional: false, nullable: false, ref: 'GalleryPost', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index f44e2c7afc..b0fad1eff2 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -33,6 +33,12 @@ export const meta = { id: 'e3e8e06e-be37-41f7-a5b4-87a8250288f0', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts index 52acee1cfb..adc46b2f63 100644 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -52,6 +52,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index a57774be73..ede777c890 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -26,6 +26,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index 5cd3c6584d..f378c5558e 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Hashtag', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index d4eb851054..ae6c98faf1 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -22,6 +22,12 @@ export const meta = { optional: false, nullable: false, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index 940e3bd69d..588f76fbb5 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -29,6 +29,12 @@ export const meta = { id: '110ee688-193e-4a3a-9ecf-c167b2e6981e', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index cb8065e3a6..57db4f3e6e 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -42,6 +42,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index 30f0c1b0c8..eb2289960a 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'UserDetailed', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index d324e3e64a..9347c9ca27 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -30,6 +30,12 @@ export const meta = { kind: 'permission', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 2a30e8b0c3..34b6907338 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -10,6 +10,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import ms from 'ms'; export const meta = { requireCredential: true, @@ -28,6 +29,12 @@ export const meta = { }, }, }, + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts index bf039ccd16..a1c965f603 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts @@ -10,6 +10,7 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { requireCredential: true, @@ -23,6 +24,12 @@ export const meta = { id: 'f9c54d7f-d4c2-4d3c-9a8g-a70daac86512', }, }, + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts index deb56a3ac4..eb8a63b3dc 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts @@ -11,6 +11,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { requireCredential: true, @@ -30,6 +31,12 @@ export const meta = { id: '1fb7cb09-d46a-4fff-b8df-057708cce513', }, }, + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 91c8597b1b..661fa257a6 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -49,6 +49,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 0b4faf5ef8..447fd18dcf 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -48,6 +48,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts index 603df44733..ebb25ebf1c 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -13,6 +13,12 @@ export const meta = { requireCredential: true, prohibitMoved: true, kind: 'write:account', + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index 3558035eca..49d47e1624 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'NoteFavorite', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index d492585ffa..9baa01ae1e 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -37,6 +37,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index 73a6fcc98b..350491a73a 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index d4c09426a7..19baa9726d 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -36,6 +36,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index 1b6359a633..65a19ac5ab 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Page', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index b7cafd74df..e324485277 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -42,6 +42,12 @@ export const meta = { optional: false, nullable: false, ref: 'MeDetailed', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts index d1a8eccb1d..edf570fcc5 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -15,6 +15,12 @@ export const meta = { requireCredential: true, kind: 'write:account', + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 4db1ca73c1..4111e90a19 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -16,6 +16,12 @@ export const meta = { errors: { }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index f1797cfde7..98b993d271 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -14,6 +14,12 @@ export const meta = { res: { type: 'object', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index d53c390460..4a3d38efa3 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -32,6 +32,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts index 3aa256077e..268c650f27 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts @@ -22,6 +22,12 @@ export const meta = { id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index d9a8fdd449..dff33016e0 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -22,7 +22,13 @@ export const meta = { res: { type: 'object', - } + }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index 3fe339606d..8216728549 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -17,6 +17,12 @@ export const meta = { type: 'string', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 28f158c62d..001cd22236 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -17,6 +17,12 @@ export const meta = { type: 'string', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index cf965ba0cf..62338965e0 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -21,6 +21,12 @@ export const meta = { id: '1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts index 67a99b028a..f23335ed46 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -31,7 +31,13 @@ export const meta = { }, }, }, - } + }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 8723035d84..35414ca2a9 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -10,6 +10,12 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, kind: 'write:account', + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index c05ee93c6f..07acddaa54 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -12,6 +12,12 @@ export const meta = { requireCredential: true, secure: true, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 76ad0bbe21..8b39b87a7f 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Signin', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index 74825cf9f3..e88e3fbc01 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -29,6 +29,12 @@ export const meta = { optional: false, nullable: false, ref: 'MeDetailed', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { 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 6e84603f7a..b0c736937c 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -55,6 +55,12 @@ export const meta = { latestStatus: { type: 'integer', nullable: true }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 1b1ac00670..1b76a01ca9 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -24,6 +24,12 @@ export const meta = { id: 'bae73e5a-5522-4965-ae19-3a8688e71d82', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 394c178f2a..f1c0f53fec 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -46,6 +46,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 4a0c09ff0c..5c7c84a73c 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -52,6 +52,12 @@ export const meta = { latestStatus: { type: 'integer', nullable: true }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 07a25bd82a..632fd2b671 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -26,6 +26,11 @@ export const meta = { }, }, + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index a70b587da7..d661eab364 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -34,6 +34,12 @@ export const meta = { optional: false, nullable: false, ref: 'InviteCode', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/invite/delete.ts b/packages/backend/src/server/api/endpoints/invite/delete.ts index e960ff9f4e..408200164c 100644 --- a/packages/backend/src/server/api/endpoints/invite/delete.ts +++ b/packages/backend/src/server/api/endpoints/invite/delete.ts @@ -36,6 +36,12 @@ export const meta = { id: '5eb8d909-2540-4970-90b8-dd6f86088121', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts index 2ffd41ae28..8d92c4957c 100644 --- a/packages/backend/src/server/api/endpoints/invite/limit.ts +++ b/packages/backend/src/server/api/endpoints/invite/limit.ts @@ -28,6 +28,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts index 23aefe83a2..b63b41edd3 100644 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'InviteCode', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 5460635e1d..9774fb55e6 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -19,6 +19,12 @@ export const meta = { { type: 'object', ref: 'MetaDetailed' }, ], }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index fc9a8f3ebe..c42df7ca80 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -27,6 +27,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index d11832858e..1e14bafc87 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; import { ApiError } from '../../error.js'; +import ms from 'ms'; export const meta = { tags: ['account'], @@ -37,6 +38,12 @@ export const meta = { id: '5467d020-daa9-4553-81e1-135c0c35a96d', }, }, + + // 20 calls per hour (match create) + limit: { + duration: ms('1hour'), + max: 20, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts index 23204f2829..3efbe349e0 100644 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ b/packages/backend/src/server/api/endpoints/mute/list.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Muting', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts index c04a92626f..3fb0d1b3b7 100644 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ b/packages/backend/src/server/api/endpoints/my/apps.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'App', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 9938322a2a..f6c37023e1 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -22,6 +22,14 @@ export const meta = { ref: 'Note', }, }, + + // 120 calls per minute + // 200 ms between calls + limit: { + duration: 1000 * 60, + max: 120, + minInterval: 200, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts index 94ec8c37ec..d36d1dfc15 100644 --- a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts @@ -30,6 +30,12 @@ export const meta = { id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6c', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 2654e196b2..e69ba9be7e 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 29cab9f212..b3dcdcef12 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -34,6 +34,12 @@ export const meta = { id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 37bc5cc878..80aea580ec 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -34,6 +34,12 @@ export const meta = { id: 'e1035875-9551-45ec-afa8-1ded1fcb53c8', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 2036facdba..19a6a5af54 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -9,6 +9,7 @@ import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import type { NoteFavoritesRepository } from '@/models/_.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['notes', 'favorites'], @@ -30,6 +31,12 @@ export const meta = { id: 'b625fc69-635e-45e9-86f4-dbefbef35af5', }, }, + + // 20 calls per hour (match create) + limit: { + duration: ms('1hour'), + max: 20, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index dcd971360d..4853489827 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index b6604b9798..0d9eec463b 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -42,6 +42,12 @@ export const meta = { id: '7a1b9cb6-235b-4e58-9c00-32c1796f502c', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index d660f3fb69..c45fcd7c5c 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -35,6 +35,12 @@ export const meta = { id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { @@ -98,7 +104,7 @@ export default class extends Endpoint { // eslint- } if (!ps.withBots) query.andWhere('user.isBot = FALSE'); - + if (ps.withRenotes === false) { query.andWhere(new Brackets(qb => { qb.where('note.renoteId IS NULL'); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 75be7b9888..3c66154e19 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -49,6 +49,12 @@ export const meta = { id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/like.ts b/packages/backend/src/server/api/endpoints/notes/like.ts index 593463aea0..9068de2865 100644 --- a/packages/backend/src/server/api/endpoints/notes/like.ts +++ b/packages/backend/src/server/api/endpoints/notes/like.ts @@ -34,6 +34,12 @@ export const meta = { id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index d4c806d7e2..1f986079c2 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -43,6 +43,12 @@ export const meta = { id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 5558dd3a8b..38912421a4 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { 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 4fd6f8682d..33a9c281b3 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Note', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts b/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts index b96691f894..69e718fc81 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts @@ -45,6 +45,12 @@ export const meta = { id: '85a5377e-b1e9-4617-b0b9-5bea73331e49', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { 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 f33f49075b..a5014a490f 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -63,6 +63,12 @@ export const meta = { id: '85a5377e-b1e9-4617-b0b9-5bea73331e49', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 7e334df93e..e683cc87bd 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -37,6 +37,12 @@ export const meta = { id: '263fff3d-d0e1-4af4-bea7-8408059b451a', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 0f0dcca605..559ca43eae 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -43,6 +43,12 @@ export const meta = { id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index a88c286f64..15f114266a 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -34,6 +34,13 @@ export const meta = { id: '12908022-2e21-46cd-ba6a-3edaf6093f46', }, }, + + // 100 calls per 10 seconds. + // This is high because the frontend calls this in a tight loop while loading timelines. + limit: { + duration: 1000 * 10, + max: 100, + }, } as const; export const paramDef = { @@ -72,7 +79,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - + if (ps.userId) { query.andWhere("user.id = :userId", { userId: ps.userId }); } diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 5f32332a6a..3f0a8157c4 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 2b4885a194..227ac0ebbf 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'Note', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index e140436d6b..eca55cd085 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -32,6 +32,12 @@ export const meta = { id: '0b44998d-77aa-4427-80d0-d2c9b8523011', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index f82ba5473d..49c51cb33c 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -29,6 +29,12 @@ export const meta = { id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { @@ -44,7 +50,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, - + private noteEntityService: NoteEntityService, private queryService: QueryService, ) { @@ -56,7 +62,7 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateBlockedUserQuery(query, me); } - + const note = await query.getOne(); if (note === null) { diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 4c1eb86542..448e704528 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -28,6 +28,12 @@ export const meta = { }, }, }, + + // 10 calls per second + limit: { + duration: 1000, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index d94d6cd652..50ce4fb89a 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -24,6 +24,12 @@ export const meta = { id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a', }, }, + + // 10 calls per hour (match create) + limit: { + duration: 1000 * 60 * 60, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index d40a04c1b1..5a46f66f9e 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -32,6 +32,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 234248db5c..61a511510c 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -46,6 +46,12 @@ export const meta = { id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 87f9b322a6..55cda135e2 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -39,6 +39,12 @@ export const meta = { id: '8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/versions.ts b/packages/backend/src/server/api/endpoints/notes/versions.ts index 2b774ae2b0..343417f0e2 100644 --- a/packages/backend/src/server/api/endpoints/notes/versions.ts +++ b/packages/backend/src/server/api/endpoints/notes/versions.ts @@ -28,6 +28,12 @@ export const meta = { id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { @@ -43,7 +49,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, - + private getterService: GetterService, private queryService: QueryService, ) { @@ -53,7 +59,7 @@ export default class extends Endpoint { // eslint- .where('note.id = :noteId', { noteId: ps.noteId }); this.queryService.generateVisibilityQuery(query, me); - + const note = await query.getOne(); if (note === null) { diff --git a/packages/backend/src/server/api/endpoints/notifications/flush.ts b/packages/backend/src/server/api/endpoints/notifications/flush.ts index 47c0642fd1..ab78435b89 100644 --- a/packages/backend/src/server/api/endpoints/notifications/flush.ts +++ b/packages/backend/src/server/api/endpoints/notifications/flush.ts @@ -13,6 +13,12 @@ export const meta = { requireCredential: true, kind: 'write:notifications', + + // 2 calls per 10 seconds + limit: { + duration: 1000 * 10, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 6565125c00..bc83f8d794 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -13,6 +13,12 @@ export const meta = { requireCredential: true, kind: 'write:notifications', + + // 2 calls per 10 seconds + limit: { + duration: 1000 * 10, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index ce454ab24a..61f9862734 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -22,6 +22,12 @@ export const meta = { id: '4a13ad31-6729-46b4-b9af-e86b265c2e74', }, }, + + // 120 calls per minute + limit: { + duration: 1000 * 60, + max: 120, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index f2bc946788..c2c3215f49 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import ms from 'ms'; export const meta = { tags: ['pages'], @@ -31,6 +32,12 @@ export const meta = { id: '8b741b3e-2c22-44b3-a15f-29949aa1601e', }, }, + + // 300 calls per hour (match update) + limit: { + duration: ms('1hour'), + max: 300, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index a47b69e56e..1886e7be28 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Page', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 11eed693ad..b92faec5e2 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -38,6 +38,12 @@ export const meta = { id: 'd4c1edbe-7da2-4eae-8714-1acfd2d63941', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index e08b832a3f..b763a471fa 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -30,6 +30,12 @@ export const meta = { id: '222120c0-3ead-4528-811b-b96f233388d7', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 70c965e0ad..727535bb4e 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -31,6 +31,12 @@ export const meta = { id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts index e218a8f755..ed6f5207a0 100644 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ b/packages/backend/src/server/api/endpoints/ping.ts @@ -21,6 +21,12 @@ export const meta = { }, }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 5b0b656c63..2e89d81404 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'UserDetailed', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 9f7d078014..2c31291243 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -24,6 +24,12 @@ export const meta = { id: 'd785b897-fcd3-4fe9-8fc3-b85c26e6c932', }, }, + + // 10 calls per 2 seconds + limit: { + duration: 1000 * 2, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index 1a584b8404..074fcbb56c 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -37,6 +37,12 @@ export const meta = { id: '2e4ef874-8bf0-4b4b-b069-4598f6d05817', }, }, + + // 20 calls per hour (match create) + limit: { + duration: 1000 * 60 * 60, + max: 20, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts index 3be01f989a..aabee237f4 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/list.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/list.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'RenoteMuting', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 67d5fabd86..bf40e1e3e0 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -20,6 +20,12 @@ export const meta = { errors: { }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 1639b57bc5..d9240dec5e 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -21,6 +21,12 @@ export const meta = { errors: { }, + + // 2 calls per 30 minutes + limit: { + duration: 1000 * 60 * 30, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/retention.ts b/packages/backend/src/server/api/endpoints/retention.ts index 4695f32042..0215af1e89 100644 --- a/packages/backend/src/server/api/endpoints/retention.ts +++ b/packages/backend/src/server/api/endpoints/retention.ts @@ -44,6 +44,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts index dd6f273e01..90e7371f91 100644 --- a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts @@ -14,6 +14,12 @@ export const meta = { errors: { }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts index 6b06068727..2cbdc26f63 100644 --- a/packages/backend/src/server/api/endpoints/reversi/games.ts +++ b/packages/backend/src/server/api/endpoints/reversi/games.ts @@ -19,6 +19,12 @@ export const meta = { optional: false, nullable: false, items: { ref: 'ReversiGameLite' }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/invitations.ts b/packages/backend/src/server/api/endpoints/reversi/invitations.ts index 5b3b9da75b..b5abad73e2 100644 --- a/packages/backend/src/server/api/endpoints/reversi/invitations.ts +++ b/packages/backend/src/server/api/endpoints/reversi/invitations.ts @@ -19,6 +19,12 @@ export const meta = { optional: false, nullable: false, items: { ref: 'UserLite' }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index aa8b8a7d72..7dd82182fa 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -34,6 +34,12 @@ export const meta = { optional: true, ref: 'ReversiGameDetailed', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts index fc3b96eb51..11931d9d3d 100644 --- a/packages/backend/src/server/api/endpoints/reversi/show-game.ts +++ b/packages/backend/src/server/api/endpoints/reversi/show-game.ts @@ -25,6 +25,12 @@ export const meta = { optional: false, nullable: false, ref: 'ReversiGameDetailed', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/surrender.ts b/packages/backend/src/server/api/endpoints/reversi/surrender.ts index 75e5372862..2b6af532e5 100644 --- a/packages/backend/src/server/api/endpoints/reversi/surrender.ts +++ b/packages/backend/src/server/api/endpoints/reversi/surrender.ts @@ -32,6 +32,12 @@ export const meta = { id: '6e04164b-a992-4c93-8489-2123069973e1', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/verify.ts b/packages/backend/src/server/api/endpoints/reversi/verify.ts index 981735a3d7..2f43c91d77 100644 --- a/packages/backend/src/server/api/endpoints/reversi/verify.ts +++ b/packages/backend/src/server/api/endpoints/reversi/verify.ts @@ -30,6 +30,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index b087aa242b..44dbe97b1d 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Role', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 71f2782a5d..b3c73e0391 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -37,6 +37,12 @@ export const meta = { ref: 'Note', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts index 38477c5e8e..dd9f9481d4 100644 --- a/packages/backend/src/server/api/endpoints/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -28,6 +28,12 @@ export const meta = { optional: false, nullable: false, ref: 'Role', }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 48d350af59..774a6f889b 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -43,6 +43,12 @@ export const meta = { required: ['id', 'user'], }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 8301c85f2e..e765163f8e 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -63,6 +63,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sponsors.ts b/packages/backend/src/server/api/endpoints/sponsors.ts index 2a8a461a8f..401d9292bc 100644 --- a/packages/backend/src/server/api/endpoints/sponsors.ts +++ b/packages/backend/src/server/api/endpoints/sponsors.ts @@ -13,6 +13,12 @@ export const meta = { requireCredential: false, requireCredentialPrivateMode: false, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index 1e6983177f..907dddb9d3 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -49,6 +49,12 @@ export const meta = { }, }, }, + + // 3 call per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index fd76df2d3c..f447b5598b 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -45,6 +45,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts index 797e4fd34d..c8783cded8 100644 --- a/packages/backend/src/server/api/endpoints/sw/show-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts @@ -34,6 +34,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index 2edf7fab1b..aa7e03dceb 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -15,6 +15,12 @@ export const meta = { requireCredential: false, description: 'Unregister from receiving push notifications.', + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts index 839a07c770..78b9323b7b 100644 --- a/packages/backend/src/server/api/endpoints/sw/update-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts @@ -43,6 +43,12 @@ export const meta = { id: ' b09d8066-8064-5613-efb6-0e963b21d012', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 9231f0ab94..e9ae9df2cc 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -40,6 +40,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index 4944be9b05..ae7902f912 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -25,6 +25,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index e845853017..089d346cd2 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'UserDetailed', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts index f7139b3684..6c0811d3f0 100644 --- a/packages/backend/src/server/api/endpoints/users/achievements.ts +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -25,6 +25,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 7f7d2ea8cc..a457a6c434 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Clip', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts index e01f19ba7a..e6acae08b1 100644 --- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'Note', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/flashs.ts b/packages/backend/src/server/api/endpoints/users/flashs.ts index e5ea450215..2da46e8747 100644 --- a/packages/backend/src/server/api/endpoints/users/flashs.ts +++ b/packages/backend/src/server/api/endpoints/users/flashs.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Flash', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index a8b4319a61..c1617e14e5 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -44,6 +44,12 @@ export const meta = { id: '3c6a84db-d619-26af-ca14-06232a21df8a', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index feda5bb353..c292c6d6a3 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -51,6 +51,12 @@ export const meta = { id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index 553886374c..931685e32a 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { 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 9248a2fa68..99568cfa12 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 @@ -47,6 +47,12 @@ export const meta = { id: 'e6965129-7b2a-40a4-bae2-cd84cd434822', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { 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 7e44d501ab..2be3197d88 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 @@ -60,6 +60,12 @@ export const meta = { id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { 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 7daf05ba4e..c3ea392e89 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -37,6 +37,12 @@ export const meta = { id: '0cf21a28-7715-4f39-a20d-777bfdb8d138', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index dc0d28a0eb..941ce77877 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -25,6 +25,12 @@ export const meta = { id: '78436795-db79-42f5-b1e2-55ea2cf19166', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts index fd142d5a01..fa898b0dc7 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -26,6 +26,12 @@ export const meta = { id: '6425bba0-985b-461e-af1b-518070e72081', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts index 6d6e8d34ea..18373fdf07 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -54,6 +54,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 4241ef1cd0..7f17863a63 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -45,6 +45,12 @@ export const meta = { id: 'ab36de0e-29e9-48cb-9732-d82f1281620d', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 94f06f3bea..1eb4d4ef42 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -35,6 +35,12 @@ export const meta = { id: '588e7f72-c744-4a61-b180-d354e912bda2', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index c717b3959c..4ba0fea314 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -59,6 +59,12 @@ export const meta = { id: '2dd9752e-a338-413d-8eec-41814430989b', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 8756801fe4..c7f4128b56 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -32,6 +32,12 @@ export const meta = { id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts index 3f4bd5af8c..4d38f7d0a7 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -25,6 +25,12 @@ export const meta = { id: '835c4b27-463d-4cfa-969b-a9058678d465', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts index 3948ae1685..0539fadd35 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts @@ -33,6 +33,12 @@ export const meta = { id: '588e7f72-c744-4a61-b180-d354e912bda2', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index a38f84d7b0..ad2f8c02e0 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -32,6 +32,12 @@ export const meta = { id: '796666fe-3dff-4d39-becb-8a5932c1d5b7', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 263d062961..92d8032fa6 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -44,6 +44,12 @@ export const meta = { id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index bb7de0e0b5..3cb958066e 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Page', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 7805ae3288..49c1190197 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -44,6 +44,12 @@ export const meta = { id: '6b95fa98-8cf9-2350-e284-f0ffdb54a805', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 5b3b4527f7..46af1f38ac 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -29,6 +29,12 @@ export const meta = { ref: 'UserDetailed', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 1d75437b81..e659c46713 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -108,6 +108,12 @@ export const meta = { }, ], }, + + // 10 calls per 2 seconds + limit: { + duration: 1000 * 2, + max: 10, + }, } as const; export const paramDef = { 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 5ff6de37d2..f811020645 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -37,6 +37,12 @@ export const meta = { id: '35e166f5-05fb-4f87-a2d5-adb42676d48f', }, }, + + // 10 calls per minute + limit: { + duration: 1000 * 60, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 8ff952dcb5..fda56ea6fe 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'User', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 0b0136066d..2d17c91e1d 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'User', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 062326e28d..7ebca78a7d 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -56,6 +56,12 @@ export const meta = { httpStatusCode: 404, }, }, + + // 5 calls per 2 seconds + limit: { + duration: 1000 * 2, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/update-memo.ts b/packages/backend/src/server/api/endpoints/users/update-memo.ts index 5a10de0c40..35369a04e8 100644 --- a/packages/backend/src/server/api/endpoints/users/update-memo.ts +++ b/packages/backend/src/server/api/endpoints/users/update-memo.ts @@ -25,6 +25,12 @@ export const meta = { id: '6fef56f3-e765-4957-88e5-c6f65329b8a5', }, }, + + // 10 calls per second + limit: { + duration: 1000, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index e275e4b475..309a41645a 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -11042,6 +11042,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11096,6 +11102,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11161,6 +11173,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11213,6 +11231,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11259,6 +11283,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11321,6 +11351,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11375,6 +11411,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11442,6 +11484,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11624,6 +11672,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11678,6 +11732,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11730,6 +11790,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11787,6 +11853,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11845,6 +11917,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11902,6 +11980,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12080,6 +12164,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12191,6 +12281,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12243,6 +12339,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12301,6 +12403,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12359,6 +12467,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12413,6 +12527,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12484,6 +12604,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12536,6 +12662,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12599,6 +12731,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12651,6 +12789,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12703,6 +12847,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12749,6 +12899,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12813,6 +12969,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12881,6 +13043,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12943,6 +13111,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13014,6 +13188,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13081,6 +13261,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13179,6 +13365,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13260,6 +13452,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13327,6 +13525,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13412,6 +13616,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13482,6 +13692,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13551,6 +13767,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13618,6 +13840,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13687,6 +13915,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13801,6 +14035,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13857,6 +14097,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13909,6 +14155,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13955,6 +14207,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14015,6 +14273,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14069,6 +14333,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14126,6 +14396,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14178,6 +14454,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14230,6 +14512,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14276,6 +14564,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14325,6 +14619,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14393,6 +14693,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14453,6 +14759,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14506,6 +14818,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14634,6 +14952,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14687,6 +15011,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14745,6 +15075,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14800,6 +15136,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14859,6 +15201,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14994,6 +15342,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15108,6 +15462,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15166,6 +15526,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15220,6 +15586,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15277,6 +15649,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15336,6 +15714,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15392,6 +15776,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15454,6 +15844,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15500,6 +15896,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15614,6 +16016,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15677,6 +16085,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15746,6 +16160,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15803,6 +16223,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15855,6 +16281,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15914,6 +16346,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15973,6 +16411,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16328,6 +16772,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16382,6 +16832,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16445,6 +16901,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16508,6 +16970,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16560,6 +17028,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16616,6 +17090,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16662,6 +17142,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16720,6 +17206,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16835,6 +17327,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16887,6 +17385,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16941,6 +17445,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16993,6 +17503,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17106,6 +17622,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17162,6 +17684,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17224,6 +17752,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17281,6 +17815,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17334,6 +17874,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17384,6 +17930,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17451,6 +18003,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17497,6 +18055,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17553,6 +18117,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17671,6 +18241,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17886,6 +18462,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18069,6 +18651,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18138,6 +18726,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18190,6 +18784,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18836,6 +19436,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18898,6 +19504,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18956,6 +19568,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19510,6 +20128,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19568,6 +20192,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19622,6 +20252,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19666,6 +20302,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19718,6 +20360,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19831,6 +20479,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19884,6 +20538,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19943,6 +20603,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19999,6 +20665,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20056,6 +20728,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20111,6 +20789,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20165,6 +20849,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20215,6 +20905,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20270,6 +20966,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20324,6 +21026,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20383,6 +21091,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20437,6 +21151,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20870,6 +21590,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20929,6 +21655,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20996,6 +21728,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21053,6 +21791,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21105,6 +21849,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21216,6 +21966,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21268,6 +22024,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21326,6 +22088,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21374,6 +22142,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21428,6 +22202,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21476,6 +22256,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21529,6 +22315,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21589,6 +22381,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21701,6 +22499,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21759,6 +22563,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21869,6 +22679,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21927,6 +22743,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21983,6 +22805,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22047,6 +22875,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22109,6 +22943,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22163,6 +23003,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22221,6 +23067,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22484,6 +23336,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22542,6 +23400,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22617,6 +23481,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22683,6 +23553,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22749,6 +23625,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22825,6 +23707,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22895,6 +23783,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22956,6 +23850,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23014,6 +23914,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23067,6 +23973,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23119,6 +24031,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23180,6 +24098,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23233,6 +24157,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23344,6 +24274,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23408,6 +24344,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23468,6 +24410,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23540,6 +24488,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23615,6 +24569,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23669,6 +24629,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23726,6 +24692,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23836,6 +24808,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23910,6 +24888,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23972,6 +24956,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24109,6 +25099,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24260,6 +25256,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24363,6 +25365,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24407,6 +25415,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24512,6 +25526,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24643,6 +25663,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24689,6 +25715,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24741,6 +25773,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24797,6 +25835,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24849,6 +25893,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25042,6 +26092,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25088,6 +26144,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25140,6 +26202,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25194,6 +26262,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25246,6 +26320,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25368,6 +26448,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25430,6 +26516,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25478,6 +26570,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25524,6 +26622,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25576,6 +26680,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25622,6 +26732,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25676,6 +26792,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25740,6 +26862,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25802,6 +26930,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25904,6 +27038,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25956,6 +27096,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26015,6 +27161,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26069,6 +27221,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26131,6 +27289,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26190,6 +27354,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26255,6 +27425,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26306,6 +27482,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26374,6 +27556,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26429,6 +27617,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26502,6 +27696,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26562,6 +27762,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26625,6 +27831,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26689,6 +27901,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26749,6 +27967,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26808,6 +28032,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26866,6 +28096,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26919,6 +28155,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26971,6 +28213,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27025,6 +28273,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27079,6 +28333,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27195,6 +28455,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27247,6 +28513,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27299,6 +28571,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27355,6 +28633,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27410,6 +28694,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27465,6 +28755,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27536,6 +28832,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27616,6 +28918,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27676,6 +28984,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27736,6 +29050,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27798,6 +29118,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27854,6 +29180,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27929,6 +29261,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27982,6 +29320,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28040,6 +29384,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28104,6 +29454,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28162,6 +29518,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28219,6 +29581,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28273,6 +29641,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28373,6 +29747,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28490,6 +29870,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28544,6 +29930,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28663,6 +30055,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28715,6 +30113,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28775,6 +30179,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28837,6 +30247,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28883,6 +30299,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28937,6 +30359,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28989,6 +30417,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -29047,6 +30481,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { From dbab122a996d732986166d5e3d602698becac558 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 22 Nov 2024 15:26:55 -0500 Subject: [PATCH 105/154] fix typo "to many requests" --- .../src/server/api/openapi/gen-spec.ts | 2 +- packages/misskey-js/src/autogen/types.ts | 132 +++++++++--------- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index fb5954fee0..82ee0f47d7 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -183,7 +183,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { }, ...(endpoint.meta.limit ? { '429': { - description: 'To many requests', + description: 'Too many requests', content: { 'application/json': { schema: { diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index e275e4b475..d64d604b10 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -10976,7 +10976,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -11495,7 +11495,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -11562,7 +11562,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -11956,7 +11956,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -12016,7 +12016,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -12139,7 +12139,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -13741,7 +13741,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -14576,7 +14576,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -14923,7 +14923,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -15050,7 +15050,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -15545,7 +15545,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16028,7 +16028,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16088,7 +16088,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16151,7 +16151,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16210,7 +16210,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16270,7 +16270,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16777,7 +16777,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17052,7 +17052,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17613,7 +17613,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17760,7 +17760,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17827,7 +17827,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17940,7 +17940,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17999,7 +17999,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18244,7 +18244,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18303,7 +18303,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18354,7 +18354,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18405,7 +18405,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18466,7 +18466,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18517,7 +18517,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18568,7 +18568,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18619,7 +18619,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18670,7 +18670,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18721,7 +18721,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18772,7 +18772,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19009,7 +19009,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19069,7 +19069,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19129,7 +19129,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19188,7 +19188,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19247,7 +19247,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19306,7 +19306,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19374,7 +19374,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19442,7 +19442,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19770,7 +19770,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20493,7 +20493,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20734,7 +20734,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20794,7 +20794,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21164,7 +21164,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21643,7 +21643,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21811,7 +21811,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22310,7 +22310,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22368,7 +22368,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22426,7 +22426,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -23285,7 +23285,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -23778,7 +23778,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24026,7 +24026,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24200,7 +24200,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24313,7 +24313,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24451,7 +24451,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24585,7 +24585,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24917,7 +24917,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24984,7 +24984,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -25304,7 +25304,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -25854,7 +25854,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -27133,7 +27133,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -28431,7 +28431,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -28599,7 +28599,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; From ebdfb2feb77594271e85a0c479480c21e2bd44c1 Mon Sep 17 00:00:00 2001 From: tess Date: Fri, 22 Nov 2024 19:55:50 +0100 Subject: [PATCH 106/154] Comply with type for Packed<'Note'> --- packages/backend/src/core/entities/NoteEntityService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 985245aeb1..37bca3ff31 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -162,8 +162,8 @@ export class NoteEntityService implements OnModuleInit { packedNote.reactionAcceptance = null; packedNote.reactionAndUserPairCache = undefined; packedNote.reactionCount = 0; - packedNote.reactionEmojis = undefined; - packedNote.reactions = undefined; + packedNote.reactionEmojis = {}; + packedNote.reactions = {}; packedNote.isHidden = true; } } From ab992422a8d52f2a67af988aab5dd5b98a439f7e Mon Sep 17 00:00:00 2001 From: dakkar Date: Fri, 22 Nov 2024 22:59:13 +0000 Subject: [PATCH 107/154] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 76cfbadb23..b3cf3f13a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.9.3", + "version": "2024.9.4", "codename": "shonk", "repository": { "type": "git", From 8e07eb7f44b948587a4a0436c84ddeeb327740c5 Mon Sep 17 00:00:00 2001 From: dakkar Date: Fri, 22 Nov 2024 23:14:37 +0000 Subject: [PATCH 108/154] remove duplicate `limit` the `users/lists/push` endpoint already has a limit, of 30/hour --- .../backend/src/server/api/endpoints/users/lists/push.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 4ba0fea314..c717b3959c 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -59,12 +59,6 @@ export const meta = { id: '2dd9752e-a338-413d-8eec-41814430989b', }, }, - - // 5 calls per second - limit: { - duration: 1000, - max: 5, - }, } as const; export const paramDef = { From a51fef29c00c6520bc403c3538ad2c4a3ffb1df8 Mon Sep 17 00:00:00 2001 From: dakkar Date: Fri, 22 Nov 2024 23:25:07 +0000 Subject: [PATCH 109/154] remove `minInterval` from `FileServerService` when showing a reply, browser will request the replied-to avatar twice at the same time, and get confused if one of the requests is refused something similar seems to happen with videos and their previews --- packages/backend/src/server/FileServerService.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index be196373c4..24ce9fa358 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -629,9 +629,6 @@ export class FileServerService { // Maximum of 10 requests / 10 minutes max: 10, duration: 1000 * 60 * 10, - - // Minimum of 250 ms between each request - minInterval: 250, }; // Rate limit proxy requests From b477de1d98ecc257987daac81cc5b070d80ab515 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 22 Nov 2024 20:49:16 -0500 Subject: [PATCH 110/154] show pinned notes by default on user profiles --- packages/frontend/src/pages/user/home.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 398be8e9ab..f2621f4ad0 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -260,7 +260,7 @@ const memoDraft = ref(props.user.memo); const isEditingMemo = ref(false); const moderationNote = ref(props.user.moderationNote); const editModerationNote = ref(false); -const noteview = ref(null); +const noteview = ref('pinned'); const listenbrainzdata = ref(false); if (props.user.listenbrainz) { From 1fb1875ac3859a7bf284d45d673698c98230de18 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 23 Nov 2024 19:42:10 -0500 Subject: [PATCH 111/154] normalize AP IDs during verification --- .../activitypub/misc/check-against-url.ts | 31 +++++++------ .../test/unit/misc/check-against-url.ts | 46 ++++++++++--------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts index 34e4907267..c8ad2ce584 100644 --- a/packages/backend/src/core/activitypub/misc/check-against-url.ts +++ b/packages/backend/src/core/activitypub/misc/check-against-url.ts @@ -2,26 +2,29 @@ * SPDX-FileCopyrightText: dakkar and sharkey-project * SPDX-License-Identifier: AGPL-3.0-only */ + import type { IObject } from '../type.js'; -function getHrefFrom(one: IObject|string): string | undefined { - if (typeof(one) === 'string') return one; - return one.href; +function getHrefsFrom(one: IObject | string | undefined | (IObject | string | undefined)[]): (string | undefined)[] { + if (Array.isArray(one)) { + return one.flatMap(h => getHrefsFrom(h)); + } + return [ + typeof(one) === 'object' ? one.href : one, + ]; } export function assertActivityMatchesUrls(activity: IObject, urls: string[]) { - const idOk = activity.id !== undefined && urls.includes(activity.id); - if (idOk) return; + const expectedUrls = new Set(urls + .filter(u => URL.canParse(u)) + .map(u => new URL(u).href), + ); - const url = activity.url; - if (url) { - // `activity.url` can be an `ApObject = IObject | string | (IObject - // | string)[]`, we have to look inside it - const activityUrls = Array.isArray(url) ? url.map(getHrefFrom) : [getHrefFrom(url)]; - const goodUrl = activityUrls.find(u => u && urls.includes(u)); + const actualUrls = [activity.id, ...getHrefsFrom(activity.url)] + .filter(u => u && URL.canParse(u)) + .map(u => new URL(u as string).href); - if (goodUrl) return; + if (!actualUrls.some(u => expectedUrls.has(u))) { + throw new Error(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`); } - - throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${JSON.stringify(activity?.url)}) match location(${urls})`); } diff --git a/packages/backend/test/unit/misc/check-against-url.ts b/packages/backend/test/unit/misc/check-against-url.ts index 88edaed839..70ee957ab1 100644 --- a/packages/backend/test/unit/misc/check-against-url.ts +++ b/packages/backend/test/unit/misc/check-against-url.ts @@ -3,49 +3,53 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { IObject } from '@/core/activitypub/type.js'; import { describe, expect, test } from '@jest/globals'; +import type { IObject } from '@/core/activitypub/type.js'; import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js'; -function assertOne(activity: IObject) { +function assertOne(activity: IObject, good = 'http://good') { // return a function so we can use `.toThrow` - return () => assertActivityMatchesUrls(activity, ['good']); + return () => assertActivityMatchesUrls(activity, [good]); } describe('assertActivityMatchesUrls', () => { + it('should throw when no ids are URLs', () => { + expect(assertOne({ type: 'Test', id: 'bad' }, 'bad')).toThrow(/bad Activity/); + }); + test('id', () => { - expect(assertOne({ type: 'Test', id: 'bad' })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', id: 'good' })).not.toThrow(); + expect(assertOne({ type: 'Test', id: 'http://bad' })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', id: 'http://good' })).not.toThrow(); }); test('simple url', () => { - expect(assertOne({ type: 'Test', url: 'bad' })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: 'good' })).not.toThrow(); + expect(assertOne({ type: 'Test', url: 'http://bad' })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: 'http://good' })).not.toThrow(); }); test('array of urls', () => { - expect(assertOne({ type: 'Test', url: ['bad'] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: ['bad', 'other'] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: ['good'] })).not.toThrow(); - expect(assertOne({ type: 'Test', url: ['bad', 'good'] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: ['http://bad'] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: ['http://bad', 'http://other'] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: ['http://good'] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: ['http://bad', 'http://good'] })).not.toThrow(); }); test('array of objects', () => { - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }, { type: 'Test', href: 'other' }] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'good' }] })).not.toThrow(); - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }, { type: 'Test', href: 'good' }] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, { type: 'Test', href: 'http://other' }] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://good' }] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, { type: 'Test', href: 'http://good' }] })).not.toThrow(); }); test('mixed array', () => { - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }, 'other'] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }, 'good'] })).not.toThrow(); - expect(assertOne({ type: 'Test', url: ['bad', { type: 'Test', href: 'good' }] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, 'http://other'] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, 'http://good'] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: ['http://bad', { type: 'Test', href: 'http://good' }] })).not.toThrow(); }); test('id and url', () => { - expect(assertOne({ type: 'Test', id: 'other', url: 'bad' })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', id: 'bad', url: 'good' })).not.toThrow(); - expect(assertOne({ type: 'Test', id: 'good', url: 'bad' })).not.toThrow(); + expect(assertOne({ type: 'Test', id: 'http://other', url: 'http://bad' })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', id: 'http://bad', url: 'http://good' })).not.toThrow(); + expect(assertOne({ type: 'Test', id: 'http://good', url: 'http://bad' })).not.toThrow(); }); }); From a47590e64cd36c238d25517b640fd06197062867 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 25 Nov 2024 13:03:16 -0500 Subject: [PATCH 112/154] add shared (cross-resource) rate limit for proxy --- .../backend/src/server/FileServerService.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 24ce9fa358..a4d0588fbe 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -31,6 +31,7 @@ import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers. import { RateLimiterService } from '@/server/api/RateLimiterService.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import { AuthenticateService } from '@/server/api/AuthenticateService.js'; +import type { IEndpointMeta } from '@/server/api/endpoints.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type Limiter from 'ratelimiter'; @@ -82,7 +83,7 @@ export class FileServerService { }); fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { - if (!await this.checkRateLimit(request, reply, `/files/${request.params.key}`)) return; + if (!await this.checkRateLimit(request, reply, '/files/', request.params.key)) return; return await this.sendDriveFile(request, reply) .catch(err => this.errorHandler(request, reply, err)); @@ -109,7 +110,7 @@ export class FileServerService { keyUrl.username = ''; keyUrl.password = ''; - if (!await this.checkRateLimit(request, reply, `/proxy/${keyUrl}`)) return; + if (!await this.checkRateLimit(request, reply, '/proxy/', keyUrl.href)) return; return await this.proxyHandler(request, reply) .catch(err => this.errorHandler(request, reply, err)); @@ -603,7 +604,8 @@ export class FileServerService { Params?: Record | unknown, }>, reply: FastifyReply, - rateLimitKey: string, + group: string, + resource: string, ): Promise { const body = request.method === 'GET' ? request.query @@ -622,29 +624,48 @@ export class FileServerService { const [user] = await this.authenticateService.authenticate(token); const actor = user?.id ?? getIpHash(request.ip); + // Call both limits: the per-resource limit and the shared cross-resource limit + return await this.checkResourceLimit(reply, actor, group, resource) && await this.checkSharedLimit(reply, actor, group); + } + + private async checkResourceLimit(reply: FastifyReply, actor: string, group: string, resource: string): Promise { const limit = { // Group by resource - key: rateLimitKey, + key: `${group}${resource}`, // Maximum of 10 requests / 10 minutes max: 10, duration: 1000 * 60 * 10, }; - // Rate limit proxy requests + return await this.checkLimit(reply, actor, limit); + } + + private async checkSharedLimit(reply: FastifyReply, actor: string, group: string): Promise { + const limit = { + key: group, + + // Maximum of 3600 requests per hour, which is an average of 1 per second. + max: 3600, + duration: 1000 * 60 * 60, + }; + + return await this.checkLimit(reply, actor, limit); + } + + private async checkLimit(reply: FastifyReply, actor: string, limit: IEndpointMeta['limit'] & { key: NonNullable }): Promise { try { await this.rateLimiterService.limit(limit, actor); return true; } catch (err) { // errはLimiter.LimiterInfoであることが期待される - reply.code(429); - if (hasRateLimitInfo(err)) { const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000); // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10)); } + reply.code(429); reply.send({ message: 'Rate limit exceeded. Please try again later.', code: 'RATE_LIMIT_EXCEEDED', From ab97b916067c775467ed92d29250802f4835f649 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 26 Oct 2024 18:39:20 -0400 Subject: [PATCH 113/154] improve AP job clearing and failure logging --- .../activitypub/ApDeliverManagerService.ts | 3 +- .../src/core/activitypub/ApRendererService.ts | 4 +- .../src/core/activitypub/ApResolverService.ts | 27 ++++----- .../src/core/activitypub/JsonLdService.ts | 5 +- .../activitypub/misc/check-against-url.ts | 3 +- .../src/core/activitypub/misc/validator.ts | 8 +-- .../core/activitypub/models/ApImageService.ts | 3 +- .../core/activitypub/models/ApNoteService.ts | 58 +++++++++---------- .../activitypub/models/ApPersonService.ts | 51 ++++++++-------- .../activitypub/models/ApQuestionService.ts | 26 ++++----- packages/backend/src/core/activitypub/type.ts | 3 +- 11 files changed, 100 insertions(+), 91 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 5d07cd8e8f..f045333d2a 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; @@ -128,7 +129,7 @@ class DeliverManager { for (const following of followers) { const inbox = following.followerSharedInbox ?? following.followerInbox; - if (inbox === null) throw new Error('inbox is null'); + if (inbox === null) throw new UnrecoverableError(`inbox is null: following ${following.id}`); inboxes.set(inbox, following.followerSharedInbox != null); } } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 42ee5bc58a..4df1cee8c3 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -7,6 +7,7 @@ import { createPublicKey, randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import * as mfm from '@transfem-org/sfm-js'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; @@ -30,6 +31,7 @@ import { IdService } from '@/core/IdService.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; +import { getApId } from './type.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() @@ -106,7 +108,7 @@ export class ApRendererService { to = [`${attributedTo}/followers`]; cc = []; } else { - throw new Error('renderAnnounce: cannot render non-public note'); + throw new UnrecoverableError(`renderAnnounce: cannot render non-public note: ${getApId(object)}`); } return { diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 25ccbdac60..d4964d544d 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; @@ -15,12 +16,12 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; +import { fromTuple } from '@/misc/from-tuple.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; -import { fromTuple } from '@/misc/from-tuple.js'; export class Resolver { private history: Set; @@ -67,7 +68,7 @@ export class Resolver { if (isCollectionOrOrderedCollection(collection)) { return collection; } else { - throw new Error(`unrecognized collection type: ${collection.type}`); + throw new UnrecoverableError(`unrecognized collection type: ${collection.type}`); } } @@ -84,15 +85,15 @@ export class Resolver { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). // Avoid strange behaviour by not trying to resolve these at all. - throw new Error(`cannot resolve URL with fragment: ${value}`); + throw new UnrecoverableError(`cannot resolve URL with fragment: ${value}`); } if (this.history.has(value)) { - throw new Error('cannot resolve already resolved one'); + throw new Error(`cannot resolve already resolved URL: ${value}`); } if (this.history.size > this.recursionLimit) { - throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`); + throw new Error(`hit recursion limit: ${value}`); } this.history.add(value); @@ -103,7 +104,7 @@ export class Resolver { } if (!this.utilityService.isFederationAllowedHost(host)) { - throw new Error('Instance is blocked'); + throw new UnrecoverableError(`instance is blocked: ${value}`); } if (this.config.signToActivityPubGet && !this.user) { @@ -119,7 +120,7 @@ export class Resolver { !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' ) { - throw new Error('invalid response'); + throw new UnrecoverableError(`invalid AP object ${value}: does not have ActivityStreams context`); } // HttpRequestService / ApRequestService have already checked that @@ -127,11 +128,11 @@ export class Resolver { // object after redirects; here we double-check that no redirects // bounced between hosts if (object.id == null) { - throw new Error('invalid AP object: missing id'); + throw new UnrecoverableError(`invalid AP object ${value}: missing id`); } if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) { - throw new Error(`invalid AP object ${value}: id ${object.id} has different host`); + throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`); } return object; @@ -140,7 +141,7 @@ export class Resolver { @bindThis private resolveLocal(url: string): Promise { const parsed = this.apDbResolverService.parseUri(url); - if (!parsed.local) throw new Error('resolveLocal: not local'); + if (!parsed.local) throw new UnrecoverableError(`resolveLocal - not a local URL: ${url}`); switch (parsed.type) { case 'notes': @@ -169,7 +170,7 @@ export class Resolver { case 'follows': return this.followRequestsRepository.findOneBy({ id: parsed.id }) .then(async followRequest => { - if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID'); + if (followRequest == null) throw new UnrecoverableError(`resolveLocal - invalid follow request ID ${parsed.id}: ${url}`); const [follower, followee] = await Promise.all([ this.usersRepository.findOneBy({ id: followRequest.followerId, @@ -181,12 +182,12 @@ export class Resolver { }), ]); if (follower == null || followee == null) { - throw new Error('resolveLocal: follower or followee does not exist'); + throw new Error(`resolveLocal - follower or followee does not exist: ${url}`); } return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)); }); default: - throw new Error(`resolveLocal: type ${parsed.type} unhandled`); + throw new UnrecoverableError(`resolveLocal: type ${parsed.type} unhandled: ${url}`); } } } diff --git a/packages/backend/src/core/activitypub/JsonLdService.ts b/packages/backend/src/core/activitypub/JsonLdService.ts index 100d4fa19f..9d1e2e06cc 100644 --- a/packages/backend/src/core/activitypub/JsonLdService.ts +++ b/packages/backend/src/core/activitypub/JsonLdService.ts @@ -5,6 +5,7 @@ import * as crypto from 'node:crypto'; import { Injectable } from '@nestjs/common'; +import { UnrecoverableError } from 'bullmq'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js'; @@ -109,7 +110,7 @@ class JsonLd { @bindThis private getLoader() { return async (url: string): Promise => { - if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`); + if (!/^https?:\/\//.test(url)) throw new UnrecoverableError(`Invalid URL: ${url}`); if (this.preLoad) { if (url in PRELOADED_CONTEXTS) { @@ -148,7 +149,7 @@ class JsonLd { }, ).then(res => { if (!res.ok) { - throw new Error(`${res.status} ${res.statusText}`); + throw new Error(`JSON-LD fetch failed with ${res.status} ${res.statusText}: ${url}`); } else { return res.json(); } diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts index c8ad2ce584..edfab5a216 100644 --- a/packages/backend/src/core/activitypub/misc/check-against-url.ts +++ b/packages/backend/src/core/activitypub/misc/check-against-url.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { UnrecoverableError } from 'bullmq'; import type { IObject } from '../type.js'; function getHrefsFrom(one: IObject | string | undefined | (IObject | string | undefined)[]): (string | undefined)[] { @@ -25,6 +26,6 @@ export function assertActivityMatchesUrls(activity: IObject, urls: string[]) { .map(u => new URL(u as string).href); if (!actualUrls.some(u => expectedUrls.has(u))) { - throw new Error(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`); + throw new UnrecoverableError(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`); } } diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts index 690beeffef..4292b7e0f7 100644 --- a/packages/backend/src/core/activitypub/misc/validator.ts +++ b/packages/backend/src/core/activitypub/misc/validator.ts @@ -9,7 +9,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new Error('Validate content type of AP response: No content-type header'); + throw new Error(`invalid content type of AP response - no content-type header: ${response.url}`); } if ( contentType.startsWith('application/activity+json') || @@ -17,7 +17,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { ) { return; } - throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json'); + throw new Error(`invalid content type of AP response - content type is not application/activity+json or application/ld+json: ${response.url}`); } const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/; @@ -26,7 +26,7 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new Error('Validate content type of JSON LD: No content-type header'); + throw new Error(`invalid content type of JSON LD - no content-type header: ${response.url}`); } if ( contentType.startsWith('application/ld+json') || @@ -35,5 +35,5 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { ) { return; } - throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json'); + throw new Error(`invalid content type of JSON LD - content type is not application/ld+json or application/json: ${response.url}`); } diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 259889d945..65328ebcf0 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; @@ -47,7 +48,7 @@ export class ApImageService { public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new Error('actor has been suspended'); + throw new UnrecoverableError(`actor has been suspended: ${actor.uri}`); } const image = await this.apResolverService.createResolver().resolve(value); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index a6d2ee1887..228a693cc4 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -5,6 +5,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -177,18 +178,18 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); if (note.id == null) { - throw new Error('Refusing to create note without id'); + throw new UnrecoverableError(`Refusing to create note without id: ${entryUri}`); } if (!checkHttps(note.id)) { - throw new Error('unexpected schema of note.id: ' + note.id); + throw new UnrecoverableError(`unexpected schema of note url ${url}: ${entryUri}`); } const url = getOneApHrefNullable(note.url); if (url != null) { if (!checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + throw new UnrecoverableError('unexpected schema of note url: ' + url); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { @@ -200,7 +201,7 @@ export class ApNoteService { // 投稿者をフェッチ if (note.attributedTo == null) { - throw new Error('invalid note.attributedTo: ' + note.attributedTo); + throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo}: ${entryUri}`); } const uri = getOneApId(note.attributedTo); @@ -209,7 +210,7 @@ export class ApNoteService { // eslint-disable-next-line no-param-reassign actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined; if (actor && actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${entryUri}`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -236,7 +237,7 @@ export class ApNoteService { */ const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${entryUri}`); } //#endregion @@ -245,7 +246,7 @@ export class ApNoteService { // 解決した投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${entryUri}`); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -275,13 +276,13 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); + throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`); } return x; }) .catch(async err => { - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`); throw err; }) : null; @@ -312,7 +313,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error('quote resolve failed'); + throw new Error(`quote resolve failed: ${entryUri}`); } } } @@ -372,7 +373,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error('The note creation failed with duplication error even when there is no duplication'); + throw new Error(`The note creation failed with duplication error even when there is no duplication, ${entryUri}`); } return duplicate; } @@ -383,15 +384,14 @@ export class ApNoteService { */ @bindThis public async updateNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise { - const noteUri = typeof value === 'string' ? value : value.id; - if (noteUri == null) throw new Error('uri is null'); + const noteUri = getApId(value); // URIがこのサーバーを指しているならスキップ - if (noteUri.startsWith(this.config.url + '/')) throw new Error('uri points local'); + if (noteUri.startsWith(this.config.url + '/')) throw new UnrecoverableError(`uri points local: ${noteUri}`); //#region このサーバーに既に登録されているか - const UpdatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); - if (UpdatedNote == null) throw new Error('Note is not registered'); + const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); + if (updatedNote == null) throw new Error(`Note is not registered: ${noteUri}`); const user = await this.usersRepository.findOneBy({ id: UpdatedNote.userId }) as MiRemoteUser | null; if (user == null) throw new Error('Note is not registered'); @@ -421,17 +421,17 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); if (note.id == null) { - throw new Error('Refusing to update note without id'); + throw new UnrecoverableError(`Refusing to update note without id: ${noteUri}`); } if (!checkHttps(note.id)) { - throw new Error('unexpected schema of note.id: ' + note.id); + throw new UnrecoverableError(`unexpected schema of note.id ${note.id}: ${noteUri}`); } const url = getOneApHrefNullable(note.url); if (url && !checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + throw new UnrecoverableError(`unexpected schema of note url ${url}: ${noteUri}`); } if (url != null) { @@ -447,7 +447,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${noteUri}`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -474,7 +474,7 @@ export class ApNoteService { */ const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${noteUri}`); } //#endregion @@ -505,13 +505,13 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); + throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`); } return x; }) .catch(async err => { - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`); throw err; }) : null; @@ -542,7 +542,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error('quote resolve failed'); + throw new Error(`quote resolve failed: ${noteUri}`); } } } @@ -577,7 +577,7 @@ export class ApNoteService { const apEmojis = emojis.map(emoji => emoji.name); try { - return await this.noteEditService.edit(actor, UpdatedNote.id, { + return await this.noteEditService.edit(actor, updatedNote.id, { createdAt: note.published ? new Date(note.published) : null, files, reply, @@ -602,7 +602,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error('The note creation failed with duplication error even when there is no duplication'); + throw new Error(`The note creation failed with duplication error even when there is no duplication: ${noteUri}`); } return duplicate; } @@ -619,7 +619,7 @@ export class ApNoteService { const uri = getApId(value); if (!this.utilityService.isFederationAllowedUri(uri)) { - throw new StatusError('blocked host', 451); + throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host'); } const unlock = await this.appLockService.getApLock(uri); @@ -631,7 +631,7 @@ export class ApNoteService { //#endregion if (this.utilityService.isUriLocal(uri)) { - throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); + throw new StatusError(`cannot resolve local note: ${uri}`, 400, 'cannot resolve local note'); } // リモートサーバーからフェッチしてきて登録 @@ -679,7 +679,7 @@ export class ApNoteService { }); const emoji = await this.emojisRepository.findOneBy({ host, name }); - if (emoji == null) throw new Error('emoji update failed'); + if (emoji == null) throw new Error(`emoji update failed: ${name}:${host}`); return emoji; } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index c552ed8b62..33373aa87e 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { AbortError } from 'node-fetch'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -140,19 +141,19 @@ export class ApPersonService implements OnModuleInit { const expectHost = this.utilityService.punyHost(uri); if (!isActor(x)) { - throw new Error(`invalid Actor type '${x.type}'`); + throw new UnrecoverableError(`invalid Actor type '${x.type}': ${uri}`); } if (!(typeof x.id === 'string' && x.id.length > 0)) { - throw new Error('invalid Actor: wrong id'); + throw new UnrecoverableError(`invalid Actor - wrong id: ${uri}`); } if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { - throw new Error('invalid Actor: wrong inbox'); + throw new UnrecoverableError(`invalid Actor - wrong inbox: ${uri}`); } if (this.utilityService.punyHost(x.inbox) !== expectHost) { - throw new Error('invalid Actor: inbox has different host'); + throw new UnrecoverableError(`invalid Actor - inbox has different host: ${uri}`); } const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); @@ -169,16 +170,16 @@ export class ApPersonService implements OnModuleInit { const collectionUri = getApId(xCollection); if (typeof collectionUri === 'string' && collectionUri.length > 0) { if (this.utilityService.punyHost(collectionUri) !== expectHost) { - throw new Error(`invalid Actor: ${collection} has different host`); + throw new UnrecoverableError(`invalid Actor - ${collection} has different host: ${uri}`); } } else if (collectionUri != null) { - throw new Error(`invalid Actor: wrong ${collection}`); + throw new UnrecoverableError(`invalid Actor: wrong ${collection} in ${uri}`); } } } if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { - throw new Error('invalid Actor: wrong username'); + throw new UnrecoverableError(`invalid Actor - wrong username: ${uri}`); } // These fields are only informational, and some AP software allows these @@ -186,7 +187,7 @@ export class ApPersonService implements OnModuleInit { // we can at least see these users and their activities. if (x.name) { if (!(typeof x.name === 'string' && x.name.length > 0)) { - throw new Error('invalid Actor: wrong name'); + throw new UnrecoverableError(`invalid Actor - wrong name: ${uri}`); } x.name = truncate(x.name, nameLength); } else if (x.name === '') { @@ -195,24 +196,24 @@ export class ApPersonService implements OnModuleInit { } if (x.summary) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) { - throw new Error('invalid Actor: wrong summary'); + throw new UnrecoverableError(`invalid Actor - wrong summary: ${uri}`); } x.summary = truncate(x.summary, summaryLength); } const idHost = this.utilityService.punyHost(x.id); if (idHost !== expectHost) { - throw new Error('invalid Actor: id has different host'); + throw new UnrecoverableError(`invalid Actor - id has different host: ${uri}`); } if (x.publicKey) { if (typeof x.publicKey.id !== 'string') { - throw new Error('invalid Actor: publicKey.id is not a string'); + throw new UnrecoverableError(`invalid Actor - publicKey.id is not a string: ${uri}`); } const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id); if (publicKeyIdHost !== expectHost) { - throw new Error('invalid Actor: publicKey.id has different host'); + throw new UnrecoverableError(`invalid Actor - publicKey.id has different host: ${uri}`); } } @@ -298,18 +299,18 @@ export class ApPersonService implements OnModuleInit { */ @bindThis public async createPerson(uri: string, resolver?: Resolver): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); + if (typeof uri !== 'string') throw new UnrecoverableError(`uri is not string: ${uri}`); const host = this.utilityService.punyHost(uri); if (host === this.utilityService.toPuny(this.config.host)) { - throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); + throw new StatusError(`cannot resolve local user: ${uri}`, 400, 'cannot resolve local user'); } // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(uri); - if (object.id == null) throw new Error('invalid object.id: ' + object.id); + if (object.id == null) throw new UnrecoverableError(`null object.id: ${uri}`); const person = this.validateActor(object, uri); @@ -341,16 +342,16 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); if (person.id == null) { - throw new Error('Refusing to create person without id'); + throw new UnrecoverableError(`Refusing to create person without id: ${uri}`); } if (url != null) { if (!checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); + throw new UnrecoverableError(`unexpected schema of person url ${url}: ${uri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`); + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id}`); } } @@ -442,7 +443,7 @@ export class ApPersonService implements OnModuleInit { if (isDuplicateKeyValueError(e)) { // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 const u = await this.usersRepository.findOneBy({ uri: person.id }); - if (u == null) throw new Error('already registered'); + if (u == null) throw new UnrecoverableError(`already registered a user with conflicting data: ${uri}`); user = u as MiRemoteUser; } else { @@ -451,7 +452,7 @@ export class ApPersonService implements OnModuleInit { } } - if (user == null) throw new Error('failed to create user: user is null'); + if (user == null) throw new Error(`failed to create user - user is null: ${uri}`); // Register to the cache this.cacheService.uriPersonCache.set(user.uri, user); @@ -500,7 +501,7 @@ export class ApPersonService implements OnModuleInit { */ @bindThis public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); + if (typeof uri !== 'string') throw new UnrecoverableError('uri is not string'); // URIがこのサーバーを指しているならスキップ if (this.utilityService.isUriLocal(uri)) return; @@ -553,16 +554,16 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); if (person.id == null) { - throw new Error('Refusing to update person without id'); + throw new UnrecoverableError(`Refusing to update person without id: ${uri}`); } if (url != null) { if (!checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); + throw new UnrecoverableError(`unexpected schema of person url ${url}: ${uri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`); + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id}`); } } @@ -732,7 +733,7 @@ export class ApPersonService implements OnModuleInit { }); if (!collection) return; - if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); + if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection: ${user.uri}`); // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 83a98d17f9..2a45576e84 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -12,7 +13,7 @@ import type { MiRemoteUser } from '@/models/User.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { getOneApId, isQuestion } from '../type.js'; +import { getApId, getApType, getOneApId, isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; @@ -48,10 +49,10 @@ export class ApQuestionService { if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(source); - if (!isQuestion(question)) throw new Error('invalid type'); + if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getApId(source)}`); const multiple = question.oneOf === undefined; - if (multiple && question.anyOf === undefined) throw new Error('invalid question'); + if (multiple && question.anyOf === undefined) throw new Error(`invalid question - neither oneOf nor anyOf is defined: ${getApId(source)}`); const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; @@ -72,21 +73,20 @@ export class ApQuestionService { */ @bindThis public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise { - const uri = typeof value === 'string' ? value : value.id; - if (uri == null) throw new Error('uri is null'); + const uri = getApId(value); // URIがこのサーバーを指しているならスキップ - if (this.utilityService.isUriLocal(uri)) throw new Error('uri points local'); + if (this.utilityService.isUriLocal(uri)) throw new Error(`uri points local: ${uri}`); //#region このサーバーに既に登録されているか const note = await this.notesRepository.findOneBy({ uri }); - if (note == null) throw new Error('Question is not registered'); + if (note == null) throw new Error(`Question is not registered (no note): ${uri}`); const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error('Question is not registered'); + if (poll == null) throw new Error(`Question is not registered (no poll): ${uri}`); const user = await this.usersRepository.findOneBy({ id: poll.userId }); - if (user == null) throw new Error('Question is not registered'); + if (user == null) throw new Error(`Question is not registered (no user): ${uri}`); //#endregion // resolve new Question object @@ -95,25 +95,25 @@ export class ApQuestionService { const question = await resolver.resolve(value); this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - if (!isQuestion(question)) throw new Error('object is not a Question'); + if (!isQuestion(question)) throw new UnrecoverableError(`object ${getApType(question)} is not a Question: ${uri}`); const attribution = (question.attributedTo) ? getOneApId(question.attributedTo) : user.uri; const attributionMatchesExisting = attribution === user.uri; const actorMatchesAttribution = (actor) ? attribution === actor.uri : true; if (!attributionMatchesExisting || !actorMatchesAttribution) { - throw new Error('Refusing to ingest update for poll by different user'); + throw new UnrecoverableError(`Refusing to ingest update for poll by different user: ${uri}`); } const apChoices = question.oneOf ?? question.anyOf; - if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices); + if (apChoices == null) throw new UnrecoverableError(`poll has no choices: ${uri}`); let changed = false; for (const choice of poll.choices) { const oldCount = poll.votes[poll.choices.indexOf(choice)]; const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems; - if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new Error('invalid newCount: ' + newCount); + if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new UnrecoverableError(`invalid newCount: ${newCount} in ${uri}`); if (oldCount <= newCount) { changed = true; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 08758aec80..56d2c3817f 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { UnrecoverableError } from 'bullmq'; import { fromTuple } from '@/misc/from-tuple.js'; export type Obj = { [x: string]: any }; @@ -61,7 +62,7 @@ export function getApId(value: string | IObject | [string | IObject]): string { if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error('cannot determine id'); + throw new UnrecoverableError('cannot determine id'); } /** From 2afbd251e11f38993accab3871fe12b7ad4c960e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 21:02:41 -0400 Subject: [PATCH 114/154] avoid potential crash if Question activity is corrupt --- .../src/core/activitypub/models/ApQuestionService.ts | 6 +++--- packages/backend/src/core/activitypub/type.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 2a45576e84..6a0b3520fc 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -13,7 +13,7 @@ import type { MiRemoteUser } from '@/models/User.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { getApId, getApType, getOneApId, isQuestion } from '../type.js'; +import { getApId, getApType, getNullableApId, getOneApId, isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; @@ -49,10 +49,10 @@ export class ApQuestionService { if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(source); - if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getApId(source)}`); + if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getNullableApId(question)}`); const multiple = question.oneOf === undefined; - if (multiple && question.anyOf === undefined) throw new Error(`invalid question - neither oneOf nor anyOf is defined: ${getApId(source)}`); + if (multiple && question.anyOf === undefined) throw new Error(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`); const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 56d2c3817f..dd4475f475 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -65,6 +65,18 @@ export function getApId(value: string | IObject | [string | IObject]): string { throw new UnrecoverableError('cannot determine id'); } +/** + * Get ActivityStreams Object id + */ +export function getNullableApId(value: string | IObject | [string | IObject]): string | null { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + + if (typeof value === 'string') return value; + if (typeof value.id === 'string') return value.id; + return null; +} + /** * Get ActivityStreams Object type * From b9fd7e1b77097c4c1e1004e6cff5499a97ff03d6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 21:03:23 -0400 Subject: [PATCH 115/154] clarify "failed to resolve quote" message --- packages/backend/src/core/activitypub/models/ApNoteService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 228a693cc4..2e232f49a2 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -313,7 +313,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error(`quote resolve failed: ${entryUri}`); + throw new Error(`failed to resolve quote for ${entryUri}`); } } } @@ -542,7 +542,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error(`quote resolve failed: ${noteUri}`); + throw new Error(`failed to resolve quote for ${noteUri}`); } } } From 4708c0abef8908e6cccd7953cf499ee26636ded2 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 10:46:58 -0500 Subject: [PATCH 116/154] don't retry jobs when processing returns a non-retryable error --- packages/backend/src/core/activitypub/ApInboxService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 4b783d3fd6..7edf4133d5 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -341,7 +341,7 @@ export class ApInboxService { // 対象が4xxならスキップ if (err instanceof StatusError) { if (!err.isRetryable) { - return `Ignored announce target ${target.id} - ${err.statusCode}`; + return `skip: ignored announce target ${target.id} - ${err.statusCode}`; } return `Error in announce target ${target.id} - ${err.statusCode}`; } @@ -461,7 +461,7 @@ export class ApInboxService { return 'ok'; } catch (err) { if (err instanceof StatusError && !err.isRetryable) { - return `skip ${err.statusCode}`; + return `skip: ${err.statusCode}`; } else { throw err; } From 3f5ea11a1fbb426d55e9f6df4ce8b4b7b642e270 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 10:47:27 -0500 Subject: [PATCH 117/154] clarify logging when an inbox job is skipped or fails --- .../backend/src/queue/processors/InboxProcessorService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 260ebe0d40..0318f621e3 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -218,7 +218,11 @@ export class InboxProcessorService implements OnApplicationShutdown { try { 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}`); + if (result.startsWith('skip:')) { + this.logger.info(`inbox activity ignored: id=${activity.id} reason=${result}`); + } else { + this.logger.warn(`inbox activity failed: id=${activity.id} reason=${result}`); + } return result; } } catch (e) { From 9eb98ae8a515476d1c1590f385f5644607e65fa9 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 11:35:34 -0500 Subject: [PATCH 118/154] clarify logging for Create/Update type checks --- packages/backend/src/core/activitypub/ApInboxService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 7edf4133d5..8462f7d805 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { fromTuple } from '@/misc/from-tuple.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, 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'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -429,7 +429,7 @@ export class ApInboxService { if (isPost(object)) { await this.createNote(resolver, actor, object, false); } else { - return `Unknown type: ${getApType(object)}`; + return `skip: Unsupported type for Create: ${getApType(object)} (object ${getNullableApId(object)})`; } } @@ -832,7 +832,7 @@ export class ApInboxService { await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); return 'ok: Note updated'; } else { - return `skip: Unknown type: ${getApType(object)}`; + return `skip: Unsupported type for Update: ${getApType(object)} (object ${getNullableApId(object)})`; } } From f115116454160817d76e7784d69ad633869c53be Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 12:00:19 -0500 Subject: [PATCH 119/154] skip Delete(Note) activities when the note is already deleted --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 8462f7d805..507a765f4e 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -548,7 +548,7 @@ export class ApInboxService { const note = await this.apDbResolverService.getNoteFromApId(uri); if (note == null) { - return 'message not found'; + return 'skip: ignoring deleted note on both ends'; } if (note.userId !== actor.id) { From f4ec837d6ea1b2db6eef28dd80a366bd9fbb9791 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 12:19:10 -0500 Subject: [PATCH 120/154] clarify "unknown activity type" logging in ApInboxService.undo --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 507a765f4e..aba49ddaef 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -687,7 +687,7 @@ export class ApInboxService { if (isAnnounce(object)) return await this.undoAnnounce(actor, object); if (isAccept(object)) return await this.undoAccept(actor, object); - return `skip: unknown object type ${getApType(object)}`; + return `skip: unknown activity type ${getApType(object)}`; } @bindThis From c5f572dcfdf258848c17665c1798b6a5abd16eff Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 12:57:06 -0500 Subject: [PATCH 121/154] clarify logging when a Move (migration) is rejected --- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 33373aa87e..23e99b9170 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -674,7 +674,7 @@ export class ApPersonService implements OnModuleInit { }); } - return 'skip'; + return 'skip: too soon to migrate accounts'; } /** From 4ec6bffca70d66b0067fc12c6598fcd7292f594e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 13:01:04 -0500 Subject: [PATCH 122/154] don't suppress errors when Update(Question) or Update(Note) fails --- packages/backend/src/core/activitypub/ApInboxService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index aba49ddaef..ced8b8dc82 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -821,7 +821,7 @@ export class ApInboxService { return await this.create(actor, activity, resolver); } - await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); + await this.apQuestionService.updateQuestion(object, actor, resolver); return 'ok: Question updated'; } else if (isPost(object)) { // If we get an Update(Note) for a note that doesn't exist, then create it instead @@ -829,7 +829,7 @@ export class ApInboxService { return await this.create(actor, activity, resolver); } - await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); + await this.apNoteService.updateNote(object, actor, resolver); return 'ok: Note updated'; } else { return `skip: Unsupported type for Update: ${getApType(object)} (object ${getNullableApId(object)})`; From b951b31ef5b11b9e972a6fe8abb76bd4c97a184c Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:13:25 -0500 Subject: [PATCH 123/154] use `IdentifiableError` in `ApImageService.createImage` --- .../backend/src/core/activitypub/models/ApImageService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 65328ebcf0..c5c3b736d4 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -4,7 +4,6 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; @@ -16,6 +15,7 @@ import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { Config } from '@/config.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { isDocument, type IObject } from '../type.js'; @@ -48,7 +48,7 @@ export class ApImageService { public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new UnrecoverableError(`actor has been suspended: ${actor.uri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${actor.uri}`); } const image = await this.apResolverService.createResolver().resolve(value); From baf19420dd6a25e0a35d14e39a7a6b5620b87575 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:21:19 -0500 Subject: [PATCH 124/154] log details when a quote fails to resolve --- .../core/activitypub/models/ApNoteService.ts | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 2e232f49a2..d131519dcb 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -291,16 +291,25 @@ export class ApNoteService { let quote: MiNote | undefined | null = null; if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) { - const tryResolveNote = async (uri: string): Promise< + const tryResolveNote = async (uri: unknown): Promise< | { status: 'ok'; res: MiNote } | { status: 'permerror' | 'temperror' } > => { - if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' }; + if (typeof uri !== 'string' || !/^https?:/.test(uri)) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`); + return { status: 'permerror' }; + } try { const res = await this.resolveNote(uri, { resolver }); - if (res == null) return { status: 'permerror' }; + if (res == null) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`); + return { status: 'permerror' }; + } return { status: 'ok', res }; } catch (e) { + const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e); + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`); + return { status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; @@ -310,10 +319,10 @@ export class ApNoteService { const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); - quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); + quote = results.filter((x): x is { status: 'ok', res: MiNote, uri: string } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error(`failed to resolve quote for ${entryUri}`); + throw new Error(`temporary error resolving quote for ${entryUri}`); } } } @@ -520,16 +529,25 @@ export class ApNoteService { let quote: MiNote | undefined | null = null; if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) { - const tryResolveNote = async (uri: string): Promise< + const tryResolveNote = async (uri: unknown): Promise< | { status: 'ok'; res: MiNote } | { status: 'permerror' | 'temperror' } > => { - if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' }; + if (typeof uri !== 'string' || !/^https?:/.test(uri)) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`); + return { status: 'permerror' }; + } try { const res = await this.resolveNote(uri, { resolver }); - if (res == null) return { status: 'permerror' }; + if (res == null) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`); + return { status: 'permerror' }; + } return { status: 'ok', res }; } catch (e) { + const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e); + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`); + return { status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; @@ -539,10 +557,10 @@ export class ApNoteService { const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); - quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); + quote = results.filter((x): x is { status: 'ok', res: MiNote, uri: string } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error(`failed to resolve quote for ${noteUri}`); + throw new Error(`temporary error resolving quote for ${entryUri}`); } } } From 6f8736c1afba9e69562e74d4b2325f781102ad3a Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:26:09 -0500 Subject: [PATCH 125/154] improve comment on getNullableApId --- packages/backend/src/core/activitypub/type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index dd4475f475..85ddc20064 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -66,7 +66,7 @@ export function getApId(value: string | IObject | [string | IObject]): string { } /** - * Get ActivityStreams Object id + * Get ActivityStreams Object id, or null if not present */ export function getNullableApId(value: string | IObject | [string | IObject]): string | null { // eslint-disable-next-line no-param-reassign From 3e72d99cf92371a58db4e7e58ad9f9126c152866 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 20 Nov 2024 22:48:03 -0500 Subject: [PATCH 126/154] fix build errors in ApNoteService.ts --- .../backend/src/core/activitypub/models/ApNoteService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index d131519dcb..83ceef12bf 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -182,7 +182,7 @@ export class ApNoteService { } if (!checkHttps(note.id)) { - throw new UnrecoverableError(`unexpected schema of note url ${url}: ${entryUri}`); + throw new UnrecoverableError(`unexpected schema of note note.id ${note.id}: ${entryUri}`); } const url = getOneApHrefNullable(note.url); @@ -402,7 +402,7 @@ export class ApNoteService { const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); if (updatedNote == null) throw new Error(`Note is not registered: ${noteUri}`); - const user = await this.usersRepository.findOneBy({ id: UpdatedNote.userId }) as MiRemoteUser | null; + const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null; if (user == null) throw new Error('Note is not registered'); // eslint-disable-next-line no-param-reassign @@ -456,7 +456,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${noteUri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${actor.id} has been suspended: ${noteUri}`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); From 43d87270d99a321d5dc9c01c0d56e769f0ce8ed6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 21 Nov 2024 10:55:27 -0500 Subject: [PATCH 127/154] improve AP error formtting --- .../src/core/activitypub/ApInboxService.ts | 4 +- .../core/activitypub/models/ApNoteService.ts | 28 ++++++------- .../activitypub/models/ApPersonService.ts | 41 ++++++++++--------- .../activitypub/models/ApQuestionService.ts | 2 +- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index ced8b8dc82..5d0404d24e 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -429,7 +429,7 @@ export class ApInboxService { if (isPost(object)) { await this.createNote(resolver, actor, object, false); } else { - return `skip: Unsupported type for Create: ${getApType(object)} (object ${getNullableApId(object)})`; + return `skip: Unsupported type for Create: ${getApType(object)} ${getNullableApId(object)}`; } } @@ -832,7 +832,7 @@ export class ApInboxService { await this.apNoteService.updateNote(object, actor, resolver); return 'ok: Note updated'; } else { - return `skip: Unsupported type for Update: ${getApType(object)} (object ${getNullableApId(object)})`; + return `skip: Unsupported type for Update: ${getApType(object)} ${getNullableApId(object)}`; } } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 83ceef12bf..cd27e562a8 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -182,18 +182,18 @@ export class ApNoteService { } if (!checkHttps(note.id)) { - throw new UnrecoverableError(`unexpected schema of note note.id ${note.id}: ${entryUri}`); + throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`); } const url = getOneApHrefNullable(note.url); if (url != null) { if (!checkHttps(url)) { - throw new UnrecoverableError('unexpected schema of note url: ' + url); + throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { - throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id}`); + throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`); } } @@ -201,7 +201,7 @@ export class ApNoteService { // 投稿者をフェッチ if (note.attributedTo == null) { - throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo}: ${entryUri}`); + throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo} in ${entryUri}`); } const uri = getOneApId(note.attributedTo); @@ -276,13 +276,13 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`); + throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`); } return x; }) .catch(async err => { - this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`); + this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`); throw err; }) : null; @@ -319,7 +319,7 @@ export class ApNoteService { const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); - quote = results.filter((x): x is { status: 'ok', res: MiNote, uri: string } => x.status === 'ok').map(x => x.res).at(0); + quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { throw new Error(`temporary error resolving quote for ${entryUri}`); @@ -382,7 +382,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error(`The note creation failed with duplication error even when there is no duplication, ${entryUri}`); + throw new Error(`The note creation failed with duplication error even when there is no duplication: ${entryUri}`); } return duplicate; } @@ -400,10 +400,10 @@ export class ApNoteService { //#region このサーバーに既に登録されているか const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); - if (updatedNote == null) throw new Error(`Note is not registered: ${noteUri}`); + if (updatedNote == null) throw new Error(`Note is not registered (no note): ${noteUri}`); const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null; - if (user == null) throw new Error('Note is not registered'); + if (user == null) throw new Error(`Note is not registered (no user): ${noteUri}`); // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); @@ -434,7 +434,7 @@ export class ApNoteService { } if (!checkHttps(note.id)) { - throw new UnrecoverableError(`unexpected schema of note.id ${note.id}: ${noteUri}`); + throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`); } const url = getOneApHrefNullable(note.url); @@ -445,11 +445,11 @@ export class ApNoteService { if (url != null) { if (!checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { - throw new Error(`note url <> id host mismatch: ${url} <> ${note.id}`); + throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`); } } @@ -557,7 +557,7 @@ export class ApNoteService { const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); - quote = results.filter((x): x is { status: 'ok', res: MiNote, uri: string } => x.status === 'ok').map(x => x.res).at(0); + quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { throw new Error(`temporary error resolving quote for ${entryUri}`); diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 23e99b9170..cd6078b2ed 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -141,26 +141,27 @@ export class ApPersonService implements OnModuleInit { const expectHost = this.utilityService.punyHost(uri); if (!isActor(x)) { - throw new UnrecoverableError(`invalid Actor type '${x.type}': ${uri}`); + throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`); } if (!(typeof x.id === 'string' && x.id.length > 0)) { - throw new UnrecoverableError(`invalid Actor - wrong id: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong id type`); } if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { - throw new UnrecoverableError(`invalid Actor - wrong inbox: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`); } - if (this.utilityService.punyHost(x.inbox) !== expectHost) { - throw new UnrecoverableError(`invalid Actor - inbox has different host: ${uri}`); + const inboxHost = this.utilityService.punyHost(x.inbox); + if (inboxHost !== expectHost) { + throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`); } const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); if (sharedInboxObject != null) { const sharedInbox = getApId(sharedInboxObject); if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) { - throw new Error('invalid Actor: wrong shared inbox'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`); } } @@ -170,16 +171,16 @@ export class ApPersonService implements OnModuleInit { const collectionUri = getApId(xCollection); if (typeof collectionUri === 'string' && collectionUri.length > 0) { if (this.utilityService.punyHost(collectionUri) !== expectHost) { - throw new UnrecoverableError(`invalid Actor - ${collection} has different host: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`); } } else if (collectionUri != null) { - throw new UnrecoverableError(`invalid Actor: wrong ${collection} in ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} type`); } } } if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { - throw new UnrecoverableError(`invalid Actor - wrong username: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong username`); } // These fields are only informational, and some AP software allows these @@ -187,7 +188,7 @@ export class ApPersonService implements OnModuleInit { // we can at least see these users and their activities. if (x.name) { if (!(typeof x.name === 'string' && x.name.length > 0)) { - throw new UnrecoverableError(`invalid Actor - wrong name: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong name`); } x.name = truncate(x.name, nameLength); } else if (x.name === '') { @@ -196,24 +197,24 @@ export class ApPersonService implements OnModuleInit { } if (x.summary) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) { - throw new UnrecoverableError(`invalid Actor - wrong summary: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong summary`); } x.summary = truncate(x.summary, summaryLength); } const idHost = this.utilityService.punyHost(x.id); if (idHost !== expectHost) { - throw new UnrecoverableError(`invalid Actor - id has different host: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`); } if (x.publicKey) { if (typeof x.publicKey.id !== 'string') { - throw new UnrecoverableError(`invalid Actor - publicKey.id is not a string: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`); } const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id); if (publicKeyIdHost !== expectHost) { - throw new UnrecoverableError(`invalid Actor - publicKey.id has different host: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`); } } @@ -310,7 +311,7 @@ export class ApPersonService implements OnModuleInit { if (resolver == null) resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(uri); - if (object.id == null) throw new UnrecoverableError(`null object.id: ${uri}`); + if (object.id == null) throw new UnrecoverableError(`null object.id in ${uri}`); const person = this.validateActor(object, uri); @@ -347,11 +348,11 @@ export class ApPersonService implements OnModuleInit { if (url != null) { if (!checkHttps(url)) { - throw new UnrecoverableError(`unexpected schema of person url ${url}: ${uri}`); + throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id}`); + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); } } @@ -559,11 +560,11 @@ export class ApPersonService implements OnModuleInit { if (url != null) { if (!checkHttps(url)) { - throw new UnrecoverableError(`unexpected schema of person url ${url}: ${uri}`); + throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id}`); + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); } } @@ -733,7 +734,7 @@ export class ApPersonService implements OnModuleInit { }); if (!collection) return; - if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection: ${user.uri}`); + if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection in ${user.uri}`); // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 6a0b3520fc..79a4f49d92 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -52,7 +52,7 @@ export class ApQuestionService { if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getNullableApId(question)}`); const multiple = question.oneOf === undefined; - if (multiple && question.anyOf === undefined) throw new Error(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`); + if (multiple && question.anyOf === undefined) throw new UnrecoverableError(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`); const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; From face6527f2a59c473da29a076c44e1592c8ef9d6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 21 Nov 2024 10:55:42 -0500 Subject: [PATCH 128/154] remove duplicate check for note.url --- packages/backend/src/core/activitypub/models/ApNoteService.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index cd27e562a8..3d4a33ded2 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -439,10 +439,6 @@ export class ApNoteService { const url = getOneApHrefNullable(note.url); - if (url && !checkHttps(url)) { - throw new UnrecoverableError(`unexpected schema of note url ${url}: ${noteUri}`); - } - if (url != null) { if (!checkHttps(url)) { throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`); From 1e99782666397de786093b36077011ac32cea1a8 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 26 Nov 2024 09:05:04 -0500 Subject: [PATCH 129/154] allow anonymous activities (resolves #819) --- packages/backend/src/queue/processors/InboxProcessorService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 260ebe0d40..5411eb69d0 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -194,7 +194,8 @@ export class InboxProcessorService implements OnApplicationShutdown { throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`); } } else { - throw new Bull.UnrecoverableError('skip: activity id is not a string'); + // Activity ID should only be string or undefined. + delete activity.id; } // Update stats From 4f1694cd99100e65455c5bf8f395f5aa9a494ecb Mon Sep 17 00:00:00 2001 From: amy Date: Tue, 26 Nov 2024 22:57:28 +0330 Subject: [PATCH 130/154] added mutual and following to user popup --- packages/frontend/src/components/MkUserPopup.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 4ae23de62c..40199c12ff 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -14,7 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.followsYou }} + {{ i18n.ts.mutuals }} + {{ i18n.ts.followsYou }} + {{ i18n.ts.following }}
From f1168f0165e595a3c49d5777628e78a754ad0274 Mon Sep 17 00:00:00 2001 From: piuvas Date: Tue, 26 Nov 2024 20:31:20 -0300 Subject: [PATCH 131/154] add profile link to aliases --- packages/backend/src/server/WellKnownServerService.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 8e326da89a..efa9af964c 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -140,6 +140,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { } const subject = `acct:${user.username}@${this.config.host}`; + const profileLink = `${this.config.url}/@${user.username}`; const self = { rel: 'self', type: 'application/activity+json', @@ -148,7 +149,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { const profilePage = { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', - href: `${this.config.url}/@${user.username}`, + href: profileLink, }; const subscribe = { rel: 'http://ostatus.org/schema/1.0/subscribe', @@ -164,12 +165,14 @@ fastify.get('/.well-known/change-password', async (request, reply) => { { element: 'Subject', value: subject }, { element: 'Link', attributes: self }, { element: 'Link', attributes: profilePage }, - { element: 'Link', attributes: subscribe }); + { element: 'Link', attributes: subscribe }, + { element: 'Alias', attributes: profileLink }); } else { reply.type(jrd); return { subject, links: [self, profilePage, subscribe], + aliases: [profileLink], }; } }); From 2e3eaaddccaf9b3dd54392861a266decbd082eb6 Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 27 Nov 2024 09:33:20 +0000 Subject: [PATCH 132/154] use a better random integer generator - fixes #810 --- packages/backend/src/misc/secure-rndstr.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts index 7853100d89..709e584ccb 100644 --- a/packages/backend/src/misc/secure-rndstr.ts +++ b/packages/backend/src/misc/secure-rndstr.ts @@ -14,11 +14,8 @@ export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string { let str = ''; for (let i = 0; i < length; i++) { - let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len); - if (rand === chars_len) { - rand = chars_len - 1; - } - str += chars.charAt(rand); + const rand = crypto.randomInt(0, chars_len); + str += chars.charAt(rand); } return str; From 57b31366e593853ed932ef64a2e88764aff57798 Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 27 Nov 2024 10:06:21 +0000 Subject: [PATCH 133/154] fix XRD+XML serialisation of `Alias` --- packages/backend/src/server/WellKnownServerService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index efa9af964c..70f672e5d9 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -166,7 +166,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { { element: 'Link', attributes: self }, { element: 'Link', attributes: profilePage }, { element: 'Link', attributes: subscribe }, - { element: 'Alias', attributes: profileLink }); + { element: 'Alias', value: profileLink }); } else { reply.type(jrd); return { From fc277839b66bd919e2c0ca0c2e2840cd3484afb0 Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 27 Nov 2024 09:57:24 +0000 Subject: [PATCH 134/154] only "publish to followers" when things really change - fixes #733 --- .../src/core/activitypub/ApRendererService.ts | 1 + .../src/server/api/endpoints/i/update.ts | 52 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 42ee5bc58a..e34872c714 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -469,6 +469,7 @@ export class ApRendererService { }; } + // if you change this, also change `server/api/endpoints/i/update.ts` @bindThis public async renderPerson(user: MiLocalUser) { const id = this.userEntityService.genLocalUserUri(user.id); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 8994c3fff6..f0e8dfc832 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -539,7 +539,9 @@ export default class extends Endpoint { // eslint- } // フォロワーにUpdateを配信 - this.accountUpdateService.publishToFollowers(user.id); + if (this.userNeedsPublishing(user, updates) || this.profileNeedsPublishing(profile, updatedProfile)) { + this.accountUpdateService.publishToFollowers(user.id); + } const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://')); for (const url of urls) { @@ -581,4 +583,52 @@ export default class extends Endpoint { // eslint- // なにもしない } } + + // these two methods need to be kept in sync with + // `ApRendererService.renderPerson` + private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial): boolean { + for (const field of ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs'] as (keyof MiUser)[]) { + if (newUser.hasOwnProperty(field) && oldUser[field] !== newUser[field]) { + return true; + } + } + for (const arrayField of ['emojis', 'tags'] as (keyof MiUser)[]) { + if (newUser.hasOwnProperty(arrayField) !== oldUser.hasOwnProperty(arrayField)) { + return true; + } + + const oldArray = oldUser[arrayField] ?? []; + const newArray = newUser[arrayField] ?? []; + if (!Array.isArray(oldArray) || !Array.isArray(newArray)) { + return true; + } + if (oldArray.join("\0") !== newArray.join("\0")) { + return true; + } + } + return false; + } + + private profileNeedsPublishing(oldProfile: MiUserProfile, newProfile: Partial): boolean { + for (const field of ['description', 'followedMessage', 'birthday', 'location', 'listenbrainz'] as (keyof MiUserProfile)[]) { + if (newProfile.hasOwnProperty(field) && oldProfile[field] !== newProfile[field]) { + return true; + } + } + for (const arrayField of ['fields'] as (keyof MiUserProfile)[]) { + if (newProfile.hasOwnProperty(arrayField) !== oldProfile.hasOwnProperty(arrayField)) { + return true; + } + + const oldArray = oldProfile[arrayField] ?? []; + const newArray = newProfile[arrayField] ?? []; + if (!Array.isArray(oldArray) || !Array.isArray(newArray)) { + return true; + } + if (oldArray.join("\0") !== newArray.join("\0")) { + return true; + } + } + return false; + } } From 1626e50fbfa558c195c99850adffe62d1deda51a Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 27 Nov 2024 10:56:25 +0000 Subject: [PATCH 135/154] expose video thumbnail to 3rd parties "cards" --- packages/backend/src/server/web/views/note.pug | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index a7cd0293cd..53cff6bcd3 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -24,13 +24,14 @@ block og meta(property='og:video:url' content= video.url) meta(property='og:video:secure_url' content= video.url) meta(property='og:video:type' content= video.type) + meta(property='og:image' content= video.thumbnailUrl) // FIXME: add width and height // FIXME: add embed player for Twitter if images.length meta(property='twitter:card' content='summary_large_image') each image in images meta(property='og:image' content= image.url) - else + else if !videos.length meta(property='twitter:card' content='summary') meta(property='og:image' content= avatarUrl) From 1f53eb2ed15d079492adac70d4501c78936d4e29 Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 27 Nov 2024 11:57:19 +0000 Subject: [PATCH 136/154] better poll editing - fixes #668 * editing _just the poll_ is now recognised as an actual change to the note * the "poll ended" notification job is now replaced (with potentially the new expiry time) --- packages/backend/src/core/NoteCreateService.ts | 1 + packages/backend/src/core/NoteEditService.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 892a929c41..35a2d8e290 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -629,6 +629,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.queueService.endedPollNotificationQueue.add(note.id, { noteId: note.id, }, { + jobId: `pollEnd:${note.id}`, delay, removeOnComplete: true, }); diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index e5e3c38cd3..406d134420 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -471,8 +471,9 @@ export class NoteEditService implements OnApplicationShutdown { const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id }); const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null; + const pollChanged = data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll); - if (Object.keys(update).length > 0 || filesChanged) { + if (Object.keys(update).length > 0 || filesChanged || pollChanged) { const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id }); await this.noteEditRepository.insert({ @@ -544,7 +545,7 @@ export class NoteEditService implements OnApplicationShutdown { })); } - if (data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll)) { + if (pollChanged) { // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.update(MiNote, oldnote.id, note); @@ -605,10 +606,11 @@ export class NoteEditService implements OnApplicationShutdown { if (data.poll && data.poll.expiresAt) { const delay = data.poll.expiresAt.getTime() - Date.now(); - this.queueService.endedPollNotificationQueue.remove(note.id); + this.queueService.endedPollNotificationQueue.remove(`pollEnd:${note.id}`); this.queueService.endedPollNotificationQueue.add(note.id, { noteId: note.id, }, { + jobId: `pollEnd:${note.id}`, delay, removeOnComplete: true, }); From 92db3596545c4f4196971153654f5d938772a4a5 Mon Sep 17 00:00:00 2001 From: Marie Date: Wed, 27 Nov 2024 19:26:56 +0000 Subject: [PATCH 137/154] lint --- packages/backend/src/misc/secure-rndstr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts index 709e584ccb..1fa686c0dd 100644 --- a/packages/backend/src/misc/secure-rndstr.ts +++ b/packages/backend/src/misc/secure-rndstr.ts @@ -14,8 +14,8 @@ export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string { let str = ''; for (let i = 0; i < length; i++) { - const rand = crypto.randomInt(0, chars_len); - str += chars.charAt(rand); + const rand = crypto.randomInt(0, chars_len); + str += chars.charAt(rand); } return str; From 3ea85b14a3f55d7f04b0d5c309572a83f2dea562 Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 27 Nov 2024 21:01:12 +0000 Subject: [PATCH 138/154] silence linter those objects always have the normal prototype, and can't have `hasOwnProperty` redefined, let me call it normally (otherwise I'd have to write `Object.prototype.hasOwnProperty.call(newUser, field)` and that's ugly) --- packages/backend/src/server/api/endpoints/i/update.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index f0e8dfc832..0965f4bcf4 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -588,11 +588,13 @@ export default class extends Endpoint { // eslint- // `ApRendererService.renderPerson` private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial): boolean { for (const field of ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs'] as (keyof MiUser)[]) { + /* eslint-disable-next-line no-prototype-builtins */ if (newUser.hasOwnProperty(field) && oldUser[field] !== newUser[field]) { return true; } } for (const arrayField of ['emojis', 'tags'] as (keyof MiUser)[]) { + /* eslint-disable-next-line no-prototype-builtins */ if (newUser.hasOwnProperty(arrayField) !== oldUser.hasOwnProperty(arrayField)) { return true; } @@ -611,11 +613,13 @@ export default class extends Endpoint { // eslint- private profileNeedsPublishing(oldProfile: MiUserProfile, newProfile: Partial): boolean { for (const field of ['description', 'followedMessage', 'birthday', 'location', 'listenbrainz'] as (keyof MiUserProfile)[]) { + /* eslint-disable-next-line no-prototype-builtins */ if (newProfile.hasOwnProperty(field) && oldProfile[field] !== newProfile[field]) { return true; } } for (const arrayField of ['fields'] as (keyof MiUserProfile)[]) { + /* eslint-disable-next-line no-prototype-builtins */ if (newProfile.hasOwnProperty(arrayField) !== oldProfile.hasOwnProperty(arrayField)) { return true; } From 9309872cff349060ef82cf368c81f50a0932367b Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 27 Nov 2024 21:25:54 +0000 Subject: [PATCH 139/154] simpler check for "property present" --- .../backend/src/server/api/endpoints/i/update.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 0965f4bcf4..8e61b8f784 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -588,14 +588,12 @@ export default class extends Endpoint { // eslint- // `ApRendererService.renderPerson` private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial): boolean { for (const field of ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs'] as (keyof MiUser)[]) { - /* eslint-disable-next-line no-prototype-builtins */ - if (newUser.hasOwnProperty(field) && oldUser[field] !== newUser[field]) { + if ((field in newUser) && oldUser[field] !== newUser[field]) { return true; } } for (const arrayField of ['emojis', 'tags'] as (keyof MiUser)[]) { - /* eslint-disable-next-line no-prototype-builtins */ - if (newUser.hasOwnProperty(arrayField) !== oldUser.hasOwnProperty(arrayField)) { + if ((arrayField in newUser) !== (arrayField in oldUser)) { return true; } @@ -613,14 +611,12 @@ export default class extends Endpoint { // eslint- private profileNeedsPublishing(oldProfile: MiUserProfile, newProfile: Partial): boolean { for (const field of ['description', 'followedMessage', 'birthday', 'location', 'listenbrainz'] as (keyof MiUserProfile)[]) { - /* eslint-disable-next-line no-prototype-builtins */ - if (newProfile.hasOwnProperty(field) && oldProfile[field] !== newProfile[field]) { + if ((field in newProfile) && oldProfile[field] !== newProfile[field]) { return true; } } for (const arrayField of ['fields'] as (keyof MiUserProfile)[]) { - /* eslint-disable-next-line no-prototype-builtins */ - if (newProfile.hasOwnProperty(arrayField) !== oldProfile.hasOwnProperty(arrayField)) { + if ((arrayField in newProfile) !== (arrayField in oldProfile)) { return true; } From 9f640beecc4318c934249a1699a11e6bc93760b6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 27 Nov 2024 22:52:53 -0500 Subject: [PATCH 140/154] fix megalodon unit tests --- packages/megalodon/package.json | 2 +- .../megalodon/test/integration/detector.spec.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json index bd9e4fc284..c65797b78c 100644 --- a/packages/megalodon/package.json +++ b/packages/megalodon/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc -p ./", "doc": "typedoc --out ../docs ./src", - "test": "NODE_ENV=test jest -u --maxWorkers=3" + "test": "cross-env NODE_ENV=test jest -u --maxWorkers=3" }, "engines": { "node": ">=15.0.0" diff --git a/packages/megalodon/test/integration/detector.spec.ts b/packages/megalodon/test/integration/detector.spec.ts index 86c32622e9..a7667d1c57 100644 --- a/packages/megalodon/test/integration/detector.spec.ts +++ b/packages/megalodon/test/integration/detector.spec.ts @@ -49,13 +49,14 @@ describe('detector', () => { }) }) - describe('wildebeest', () => { - const url = 'https://wildebeest.mirror-kt.dev' - it('should be mastodon', async () => { - const wildebeest = await detector(url) - expect(wildebeest).toEqual('mastodon') - }) - }) + // This domain no longer resolves, and resolution failures apparently crash jest + // describe('wildebeest', () => { + // const url = 'https://wildebeest.mirror-kt.dev' + // it('should be mastodon', async () => { + // const wildebeest = await detector(url) + // expect(wildebeest).toEqual('mastodon') + // }) + // }) describe('unknown', () => { const url = 'https://google.com' From 22bb09c6ed9f0124baae92f20229c6a0902a21f1 Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Thu, 28 Nov 2024 01:09:05 -0500 Subject: [PATCH 141/154] Bump develop version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b3cf3f13a9..6e84882440 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.9.4", + "version": "2024.10.0-dev", "codename": "shonk", "repository": { "type": "git", From 385846d43d9f8b8b4efdef1bb13055f8639d155f Mon Sep 17 00:00:00 2001 From: piuvas Date: Thu, 28 Nov 2024 18:47:20 -0300 Subject: [PATCH 142/154] make block confirm dialog localizable. --- locales/index.d.ts | 4 ++++ packages/frontend/src/scripts/get-user-menu.ts | 2 +- sharkey-locales/en-US.yml | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index d1cb1f97ea..f0f8df56bb 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11000,6 +11000,10 @@ export interface Locale extends ILocale { * Show warning when opening external URLs */ "warnExternalUrl": string; + /** + * Confirm + */ + "confirm": string; "_mfm": { /** * This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index d15279d633..55189aa481 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -104,7 +104,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter async function getConfirmed(text: string): Promise { const confirm = await os.confirm({ type: 'warning', - title: 'confirm', + title: i18n.ts.confirm, text, }); diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 163fd0b0ae..e30ce0714f 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -156,6 +156,7 @@ allowClickingNotifications: "Allow clicking on pop-up notifications" pinnedOnly: "Pinned" blockingYou: "Blocking you" warnExternalUrl: "Show warning when opening external URLs" +confirm: "Confirm" _delivery: stop: "Suspend delivery" resume: "Resume delivery" From 51bc393d589eceb60e5e70a9ab4ca1e2d95dcf6b Mon Sep 17 00:00:00 2001 From: piuvas Date: Thu, 28 Nov 2024 19:12:07 -0300 Subject: [PATCH 143/154] remove title and change dialog icon --- locales/index.d.ts | 4 ---- packages/frontend/src/scripts/get-user-menu.ts | 3 +-- sharkey-locales/en-US.yml | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index f0f8df56bb..d1cb1f97ea 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11000,10 +11000,6 @@ export interface Locale extends ILocale { * Show warning when opening external URLs */ "warnExternalUrl": string; - /** - * Confirm - */ - "confirm": string; "_mfm": { /** * This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 55189aa481..090cffe203 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -103,8 +103,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter async function getConfirmed(text: string): Promise { const confirm = await os.confirm({ - type: 'warning', - title: i18n.ts.confirm, + type: 'question', text, }); diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index e30ce0714f..163fd0b0ae 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -156,7 +156,6 @@ allowClickingNotifications: "Allow clicking on pop-up notifications" pinnedOnly: "Pinned" blockingYou: "Blocking you" warnExternalUrl: "Show warning when opening external URLs" -confirm: "Confirm" _delivery: stop: "Suspend delivery" resume: "Resume delivery" From 0efd5eff2b3b503a1cab48205a5240b08a07bacc Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 28 Nov 2024 19:17:34 -0500 Subject: [PATCH 144/154] remove unused import from InternalStorageService --- packages/backend/src/core/InternalStorageService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index f7371f8e79..5b179baf41 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -4,7 +4,7 @@ */ import * as fs from 'node:fs'; -import { copyFile, mkdir, unlink, writeFile } from 'node:fs/promises'; +import { copyFile, unlink, writeFile } from 'node:fs/promises'; import * as Path from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; From 387dc4bb4b57b5071de3f2180c591c42d3ddd5b3 Mon Sep 17 00:00:00 2001 From: dakkar Date: Sat, 23 Nov 2024 17:51:09 +0000 Subject: [PATCH 145/154] UNTESTED maybe laxer match on authority - fixes #815 --- packages/backend/package.json | 4 ++- packages/backend/src/core/UtilityService.ts | 9 ++++++ .../src/core/activitypub/ApRequestService.ts | 2 +- .../src/core/activitypub/ApResolverService.ts | 2 +- .../core/activitypub/models/ApNoteService.ts | 6 ++-- .../activitypub/models/ApPersonService.ts | 16 +++++------ pnpm-lock.yaml | 28 ++++++++++++++----- 7 files changed, 46 insertions(+), 21 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 19547c5033..00baef56d8 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -93,6 +93,7 @@ "@swc/core": "1.6.6", "@transfem-org/sfm-js": "0.24.5", "@twemoji/parser": "15.1.1", + "@types/psl": "^1.1.3", "accepts": "1.3.8", "ajv": "8.17.1", "archiver": "7.0.1", @@ -135,9 +136,9 @@ "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", + "juice": "11.0.0", "megalodon": "workspace:*", "meilisearch": "0.42.0", - "juice": "11.0.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", @@ -158,6 +159,7 @@ "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "proxy-addr": "^2.0.7", + "psl": "^1.13.0", "pug": "3.0.3", "punycode": "2.3.1", "qrcode": "1.5.4", diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 4c6d539e16..c84e7f212b 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -7,6 +7,7 @@ import { URL } from 'node:url'; import { toASCII } from 'punycode'; import { Inject, Injectable } from '@nestjs/common'; import RE2 from 're2'; +import psl from 'psl'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; @@ -122,6 +123,14 @@ export class UtilityService { return host; } + @bindThis + public punyHostPSLDomain(url: string): string { + const urlObj = new URL(url); + const domain = psl.get(urlObj.hostname) ?? urlObj.hostname; + const host = `${this.toPuny(domain)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; + return host; + } + public isFederationAllowedHost(host: string): boolean { if (this.meta.federation === 'none') return false; if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false; diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index eeff73385b..0dea615cb0 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -243,7 +243,7 @@ export class ApRequestService { if (alternate) { const href = alternate.getAttribute('href'); if (href) { - if (this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) { + if (this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) { return await this.signedGet(href, user, false); } } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index d4964d544d..e56b9ebc99 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -131,7 +131,7 @@ export class Resolver { throw new UnrecoverableError(`invalid AP object ${value}: missing id`); } - if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) { + if (this.utilityService.punyHostPSLDomain(object.id) !== this.utilityService.punyHostPSLDomain(value)) { throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`); } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 3d4a33ded2..b8aa67e9ea 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -192,8 +192,8 @@ export class ApNoteService { throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`); } - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { - throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`); + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) { + throw new UnrecoverableError(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`); } } @@ -444,7 +444,7 @@ export class ApNoteService { throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`); } - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) { throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index cd6078b2ed..598486cd84 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -138,7 +138,7 @@ export class ApPersonService implements OnModuleInit { */ @bindThis private validateActor(x: IObject, uri: string): IActor { - const expectHost = this.utilityService.punyHost(uri); + const expectHost = this.utilityService.punyHostPSLDomain(uri); if (!isActor(x)) { throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`); @@ -152,7 +152,7 @@ export class ApPersonService implements OnModuleInit { throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`); } - const inboxHost = this.utilityService.punyHost(x.inbox); + const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox); if (inboxHost !== expectHost) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`); } @@ -160,7 +160,7 @@ export class ApPersonService implements OnModuleInit { const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); if (sharedInboxObject != null) { const sharedInbox = getApId(sharedInboxObject); - if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) { + if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHostPSLDomain(sharedInbox) === expectHost)) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`); } } @@ -170,7 +170,7 @@ export class ApPersonService implements OnModuleInit { if (xCollection != null) { const collectionUri = getApId(xCollection); if (typeof collectionUri === 'string' && collectionUri.length > 0) { - if (this.utilityService.punyHost(collectionUri) !== expectHost) { + if (this.utilityService.punyHostPSLDomain(collectionUri) !== expectHost) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`); } } else if (collectionUri != null) { @@ -202,7 +202,7 @@ export class ApPersonService implements OnModuleInit { x.summary = truncate(x.summary, summaryLength); } - const idHost = this.utilityService.punyHost(x.id); + const idHost = this.utilityService.punyHostPSLDomain(x.id); if (idHost !== expectHost) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`); } @@ -212,7 +212,7 @@ export class ApPersonService implements OnModuleInit { throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`); } - const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id); + const publicKeyIdHost = this.utilityService.punyHostPSLDomain(x.publicKey.id); if (publicKeyIdHost !== expectHost) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`); } @@ -351,7 +351,7 @@ export class ApPersonService implements OnModuleInit { throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); } - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) { throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); } } @@ -563,7 +563,7 @@ export class ApPersonService implements OnModuleInit { throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); } - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) { throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2d7035550..76e399d5dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,9 @@ importers: '@twemoji/parser': specifier: 15.1.1 version: 15.1.1 + '@types/psl': + specifier: ^1.1.3 + version: 1.1.3 accepts: specifier: 1.3.8 version: 1.3.8 @@ -365,6 +368,9 @@ importers: proxy-addr: specifier: ^2.0.7 version: 2.0.7 + psl: + specifier: ^1.13.0 + version: 1.13.0 pug: specifier: 3.0.3 version: 3.0.3 @@ -4835,6 +4841,9 @@ packages: '@types/proxy-addr@2.0.3': resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==} + '@types/psl@1.1.3': + resolution: {integrity: sha512-Iu174JHfLd7i/XkXY6VDrqSlPvTDQOtQI7wNAXKKOAADJ9TduRLkNdMgjGiMxSttUIZnomv81JAbAbC0DhggxA==} + '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} @@ -5307,6 +5316,7 @@ packages: acorn-import-assertions@1.9.0: resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 @@ -9736,8 +9746,8 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + psl@1.13.0: + resolution: {integrity: sha512-BFwmFXiJoFqlUpZ5Qssolv15DMyc84gTBds1BjsV1BfXEo1UyyD7GsmN67n7J77uRhoSNW1AXtXKPLcBFQn9Aw==} pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -13103,7 +13113,7 @@ snapshots: '@eslint/config-array@0.17.1': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -13125,7 +13135,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.7 espree: 10.1.0 globals: 14.0.0 ignore: 5.3.1 @@ -15648,6 +15658,8 @@ snapshots: dependencies: '@types/node': 20.14.12 + '@types/psl@1.1.3': {} + '@types/pug@2.0.10': {} '@types/punycode@2.1.4': {} @@ -18435,7 +18447,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.7 escape-string-regexp: 4.0.0 eslint-scope: 8.0.2 eslint-visitor-keys: 4.0.0 @@ -22014,7 +22026,9 @@ snapshots: pseudomap@1.0.2: {} - psl@1.9.0: {} + psl@1.13.0: + dependencies: + punycode: 2.3.1 pstree.remy@1.1.8: {} @@ -23256,7 +23270,7 @@ snapshots: tough-cookie@4.1.4: dependencies: - psl: 1.9.0 + psl: 1.13.0 punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 From fd2af6dfe62dda3549cffe44a735314086279fc0 Mon Sep 17 00:00:00 2001 From: dakkar Date: Sat, 23 Nov 2024 17:51:33 +0000 Subject: [PATCH 146/154] silence linter? it started complaining about that `true &&` all of a sudden --- packages/backend/test/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 26de19eaf1..cf97473d14 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -612,8 +612,8 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { username: config.db.user, password: config.db.pass, database: config.db.db, - synchronize: true && !justBorrow, - dropSchema: true && !justBorrow, + synchronize: !justBorrow, + dropSchema: !justBorrow, entities: initEntities ?? entities, }); From 82376f312d2286b8acf8baf5c652ac08e6b10390 Mon Sep 17 00:00:00 2001 From: dakkar Date: Sun, 24 Nov 2024 13:51:44 +0000 Subject: [PATCH 147/154] use "userland" `punycode`, plus tests thanks to CenTdemeern1 for the `import` incantation --- packages/backend/src/core/UtilityService.ts | 6 +-- packages/backend/test/unit/UtilityService.ts | 39 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 packages/backend/test/unit/UtilityService.ts diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index c84e7f212b..fca715164e 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -4,7 +4,7 @@ */ import { URL } from 'node:url'; -import { toASCII } from 'punycode'; +import punycode from 'punycode/'; import { Inject, Injectable } from '@nestjs/common'; import RE2 from 're2'; import psl from 'psl'; @@ -107,13 +107,13 @@ export class UtilityService { @bindThis public toPuny(host: string): string { - return toASCII(host.toLowerCase()); + return punycode.toASCII(host.toLowerCase()); } @bindThis public toPunyNullable(host: string | null | undefined): string | null { if (host == null) return null; - return toASCII(host.toLowerCase()); + return punycode.toASCII(host.toLowerCase()); } @bindThis diff --git a/packages/backend/test/unit/UtilityService.ts b/packages/backend/test/unit/UtilityService.ts new file mode 100644 index 0000000000..b5e978569e --- /dev/null +++ b/packages/backend/test/unit/UtilityService.ts @@ -0,0 +1,39 @@ +import * as assert from 'assert'; +import { Test } from '@nestjs/testing'; + +import { CoreModule } from '@/core/CoreModule.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { GlobalModule } from '@/GlobalModule.js'; + +describe('UtilityService', () => { + let utilityService: UtilityService; + + beforeAll(async () => { + const app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + }).compile(); + utilityService = app.get(UtilityService); + }); + + describe('punyHost', () => { + test('simple', () => { + assert.equal(utilityService.punyHost('http://www.foo.com'),'www.foo.com'); + }); + test('japanese', () => { + assert.equal(utilityService.punyHost('http://www.新聞.com'),'www.xn--efvv70d.com'); + }); + }); + + describe('punyHostPSLDomain', () => { + test('simple', () => { + assert.equal(utilityService.punyHostPSLDomain('http://www.foo.com'),'foo.com'); + }); + test('japanese', () => { + assert.equal(utilityService.punyHostPSLDomain('http://www.新聞.com'),'xn--efvv70d.com'); + }); + test('lower', () => { + assert.equal(utilityService.punyHostPSLDomain('http://foo.github.io'),'foo.github.io'); + assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.github.io'),'bar.github.io'); + }); + }); +}); From 97d17c537b3a8522ce23190b6d0273a8c7e6f486 Mon Sep 17 00:00:00 2001 From: dakkar Date: Sun, 24 Nov 2024 14:04:03 +0000 Subject: [PATCH 148/154] spaces / lint --- packages/backend/test/unit/UtilityService.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/test/unit/UtilityService.ts b/packages/backend/test/unit/UtilityService.ts index b5e978569e..837b55206e 100644 --- a/packages/backend/test/unit/UtilityService.ts +++ b/packages/backend/test/unit/UtilityService.ts @@ -17,23 +17,23 @@ describe('UtilityService', () => { describe('punyHost', () => { test('simple', () => { - assert.equal(utilityService.punyHost('http://www.foo.com'),'www.foo.com'); + assert.equal(utilityService.punyHost('http://www.foo.com'), 'www.foo.com'); }); test('japanese', () => { - assert.equal(utilityService.punyHost('http://www.新聞.com'),'www.xn--efvv70d.com'); + assert.equal(utilityService.punyHost('http://www.新聞.com'), 'www.xn--efvv70d.com'); }); }); describe('punyHostPSLDomain', () => { test('simple', () => { - assert.equal(utilityService.punyHostPSLDomain('http://www.foo.com'),'foo.com'); + assert.equal(utilityService.punyHostPSLDomain('http://www.foo.com'), 'foo.com'); }); test('japanese', () => { - assert.equal(utilityService.punyHostPSLDomain('http://www.新聞.com'),'xn--efvv70d.com'); + assert.equal(utilityService.punyHostPSLDomain('http://www.新聞.com'), 'xn--efvv70d.com'); }); test('lower', () => { - assert.equal(utilityService.punyHostPSLDomain('http://foo.github.io'),'foo.github.io'); - assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.github.io'),'bar.github.io'); + assert.equal(utilityService.punyHostPSLDomain('http://foo.github.io'), 'foo.github.io'); + assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.github.io'), 'bar.github.io'); }); }); }); From f0139ae1e08b65354ab34fa4d422c41844bf2fdb Mon Sep 17 00:00:00 2001 From: dakkar Date: Tue, 26 Nov 2024 11:14:51 +0000 Subject: [PATCH 149/154] actually use the correct `import` syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CenTdemeern1 had told me, but I got it wrong ☹ --- packages/backend/src/core/UtilityService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index fca715164e..fc38c9dac6 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -4,7 +4,7 @@ */ import { URL } from 'node:url'; -import punycode from 'punycode/'; +import punycode from 'punycode/punycode.js'; import { Inject, Injectable } from '@nestjs/common'; import RE2 from 're2'; import psl from 'psl'; From 51afbbaf72eb88e1a1014cdf34c6b064d8490cd2 Mon Sep 17 00:00:00 2001 From: dakkar Date: Wed, 27 Nov 2024 14:44:33 +0000 Subject: [PATCH 150/154] handle `.masto.host` specially --- packages/backend/src/core/UtilityService.ts | 14 +++++++++++++- packages/backend/test/unit/UtilityService.ts | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index fc38c9dac6..dda4bd5fbd 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -123,10 +123,22 @@ export class UtilityService { return host; } + private specialSuffix(hostname: string): string | null { + // masto.host provides domain names for its clients, we have to + // treat it as if it were a public suffix + const mastoHost = hostname.match(/\.?([a-zA-Z0-9-]+\.masto\.host)$/i); + if (mastoHost) { + return mastoHost[1]; + } + + return null; + } + @bindThis public punyHostPSLDomain(url: string): string { const urlObj = new URL(url); - const domain = psl.get(urlObj.hostname) ?? urlObj.hostname; + const hostname = urlObj.hostname; + const domain = this.specialSuffix(hostname) ?? psl.get(hostname) ?? hostname; const host = `${this.toPuny(domain)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; return host; } diff --git a/packages/backend/test/unit/UtilityService.ts b/packages/backend/test/unit/UtilityService.ts index 837b55206e..d86e794f2f 100644 --- a/packages/backend/test/unit/UtilityService.ts +++ b/packages/backend/test/unit/UtilityService.ts @@ -35,5 +35,9 @@ describe('UtilityService', () => { assert.equal(utilityService.punyHostPSLDomain('http://foo.github.io'), 'foo.github.io'); assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.github.io'), 'bar.github.io'); }); + test('special', () => { + assert.equal(utilityService.punyHostPSLDomain('http://foo.masto.host'), 'foo.masto.host'); + assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.masto.host'), 'bar.masto.host'); + }); }); }); From e6e48fb6bcdc06cfd91357717c9bef1d930c2a1a Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 17:03:24 -0500 Subject: [PATCH 151/154] interpret Dislike activities as Undo(Like) --- .../backend/src/core/activitypub/ApInboxService.ts | 13 ++++++++++--- packages/backend/src/core/activitypub/type.ts | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 5d0404d24e..75ba5e97b9 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { fromTuple } from '@/misc/from-tuple.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -41,7 +41,7 @@ import { ApAudienceService } from './ApAudienceService.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; -import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; +import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IDislike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; @Injectable() export class ApInboxService { @@ -166,6 +166,8 @@ export class ApInboxService { return await this.announce(actor, activity, resolver); } else if (isLike(activity)) { return await this.like(actor, activity, resolver); + } else if (isDislike(activity)) { + return await this.dislike(actor, activity); } else if (isUndo(activity)) { return await this.undo(actor, activity, resolver); } else if (isBlock(activity)) { @@ -220,6 +222,11 @@ export class ApInboxService { } } + @bindThis + private async dislike(actor: MiRemoteUser, dislike: IDislike): Promise { + return await this.undoLike(actor, dislike); + } + @bindThis private async accept(actor: MiRemoteUser, activity: IAccept, resolver?: Resolver): Promise { const uri = activity.id ?? activity; @@ -782,7 +789,7 @@ export class ApInboxService { } @bindThis - private async undoLike(actor: MiRemoteUser, activity: ILike): Promise { + private async undoLike(actor: MiRemoteUser, activity: ILike | IDislike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 85ddc20064..c9e20e0168 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -336,6 +336,10 @@ export interface ILike extends IActivity { _misskey_reaction?: string; } +export interface IDislike extends IActivity { + type: 'Dislike'; +} + export interface IAnnounce extends IActivity { type: 'Announce'; } @@ -368,6 +372,7 @@ export const isLike = (object: IObject): object is ILike => { const type = getApType(object); return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type); }; +export const isDislike = (object: IObject): object is IDislike => getApType(object) === 'Dislike'; export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; From 3d3cf5bd7aa19aaf245aa1fe426fe39f2e7c3436 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 28 Nov 2024 19:56:26 -0500 Subject: [PATCH 152/154] add option `filePermissionBits` to override permissions on locally-stored files This is useful for custom deployments, such as using a reverse proxy to serve static files directly --- .config/ci.yml | 5 +++++ .config/cypress-devcontainer.yml | 5 +++++ .config/docker_example.yml | 5 +++++ .config/example.yml | 5 +++++ packages/backend/src/config.ts | 10 ++++++++-- packages/backend/src/core/InternalStorageService.ts | 12 ++++++++++-- 6 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.config/ci.yml b/.config/ci.yml index d20ede8d35..8730ccab3a 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -229,3 +229,8 @@ checkActivityPubGetSignature: false # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index d8013a1c95..342b0f43da 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -222,3 +222,8 @@ allowedPrivateNetworks: [ # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 5fac3dc41e..ce2daf3aec 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -312,3 +312,8 @@ checkActivityPubGetSignature: false # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/example.yml b/.config/example.yml index cf7b972de5..9debb3bf70 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -334,3 +334,8 @@ checkActivityPubGetSignature: false # PID File of master process #pidFile: /tmp/misskey.pid + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 3dc49c7eb6..9cac058d96 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -115,6 +115,7 @@ type Source = { }; pidFile: string; + filePermissionBits?: string; }; export type Config = { @@ -212,6 +213,7 @@ export type Config = { } | undefined; pidFile: string; + filePermissionBits?: string; }; const _filename = fileURLToPath(import.meta.url); @@ -347,6 +349,7 @@ export function loadConfig(): Config { deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), import: config.import, pidFile: config.pidFile, + filePermissionBits: config.filePermissionBits, }; } @@ -452,7 +455,10 @@ function applyEnvOverrides(config: Source) { } } - const alwaysStrings = { 'chmodSocket': true } as { [key: string]: boolean }; + const alwaysStrings: { [key in string]?: boolean } = { + 'chmodSocket': true, + 'filePermissionBits': true, + }; function _assign(path: (string | number)[], lastStep: string | number, value: string) { let thisConfig = config as any; @@ -490,7 +496,7 @@ function applyEnvOverrides(config: Source) { _apply_top(['sentryForBackend', 'enableNodeProfiling']); _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]); - _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile']]); + _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile', 'filePermissionBits']]); _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); _apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]); } diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 5b179baf41..b00c5796d2 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -4,7 +4,7 @@ */ import * as fs from 'node:fs'; -import { copyFile, unlink, writeFile } from 'node:fs/promises'; +import { copyFile, unlink, writeFile, chmod } from 'node:fs/promises'; import * as Path from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; @@ -41,12 +41,20 @@ export class InternalStorageService { @bindThis public async saveFromPath(key: string, srcPath: string): Promise { await copyFile(srcPath, this.resolvePath(key)); - return `${this.config.url}/files/${key}`; + return await this.finalizeSavedFile(key); } @bindThis public async saveFromBuffer(key: string, data: Buffer): Promise { await writeFile(this.resolvePath(key), data); + return await this.finalizeSavedFile(key); + } + + private async finalizeSavedFile(key: string): Promise { + if (this.config.filePermissionBits) { + const path = this.resolvePath(key); + await chmod(path, this.config.filePermissionBits); + } return `${this.config.url}/files/${key}`; } From b4b72ab2dfc5f77181ece47c01e3fa59e2b58460 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 30 Nov 2024 11:42:23 +0100 Subject: [PATCH 153/154] Opinionated changes to merge/issue templates In hopes of making them look a bit nicer by default. --- .gitlab/issue_templates/bug.md | 28 +++++++++++++--------- .gitlab/issue_templates/feature.md | 14 +++++++---- .gitlab/merge_request_templates/default.md | 7 +++--- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md index 6914647570..a909067269 100644 --- a/.gitlab/issue_templates/bug.md +++ b/.gitlab/issue_templates/bug.md @@ -3,27 +3,33 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) --> -**What happened?** _(Please give us a brief description of what happened.)_ +# **What happened?** + -**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_ +# **What did you expect to happen?** + -**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_ +# **Version** + -**Instance** _(What instance of Sharkey are you using?)_ +# **Instance** + -**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_ +# **What type of issue is this?** + -**What browser are you using? (Client-side issues only)** +# **What browser are you using? (Client-side issues only)** -**What operating system are you using? (Client-side issues only)** +# **What operating system are you using? (Client-side issues only)** -**How do you deploy Sharkey on your server? (Server-side issues only)** +# **How do you deploy Sharkey on your server? (Server-side issues only)** -**What operating system are you using? (Server-side issues only)** +# **What operating system are you using? (Server-side issues only)** -**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_ +# **Relevant log output** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar issues, and this is not a duplicate. diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md index d4235eb5a3..a77f9335fe 100644 --- a/.gitlab/issue_templates/feature.md +++ b/.gitlab/issue_templates/feature.md @@ -3,15 +3,19 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) --> -**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_ +# **What feature would you like implemented?** + -**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_ +# **Why should we add this feature?** + -**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_ +# **Version** + -**Instance** _(What instance of Sharkey are you using?)_ +# **Instance** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar requests, and this is not a duplicate. diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index 18bffa5419..e6977def70 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -1,11 +1,12 @@ -**What does this PR do?** _(Please give us a brief description of what this PR does.)_ +# **What does this MR do?** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have made sure to test this pull request +- [ ] I have made sure to test this merge request From 667b32457270db2e884e2ee73218d407e3d3a338 Mon Sep 17 00:00:00 2001 From: Werner Kroneman Date: Thu, 5 Dec 2024 12:31:58 +0200 Subject: [PATCH 154/154] Added nix flake stuff. --- flake.lock | 27 +++ flake.nix | 38 ++++ sharkey-service.nix | 418 ++++++++++++++++++++++++++++++++++++++++++++ sharkey.nix | 124 +++++++++++++ 4 files changed, 607 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 sharkey-service.nix create mode 100644 sharkey.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..9de322090e --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..e32d28bea5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: { + + nixosConfigurations.container = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = + [ + ( import ./sharkey-service.nix ) + ({ pkgs, ... }: { + boot.isContainer = true; + + # Let 'nixos-version --json' know about the Git revision + # of this flake. + system.configurationRevision = nixpkgs.lib.mkIf (self ? rev) self.rev; + + # Network configuration. + networking.useDHCP = false; + networking.firewall.allowedTCPPorts = [ 3000 ]; + + system.stateVersion = "24.04"; + + services.sharkey = { + enable = true; + package = (pkgs.callPackage ./sharkey.nix {}); + settings = { + url = "https://sharkey.localhost"; + }; + redis.createLocally = true; + database.createLocally = true; + }; + }) + ]; + }; + + }; +} diff --git a/sharkey-service.nix b/sharkey-service.nix new file mode 100644 index 0000000000..e6e004c8d1 --- /dev/null +++ b/sharkey-service.nix @@ -0,0 +1,418 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.services.sharkey; + settingsFormat = pkgs.formats.yaml { }; + redisType = lib.types.submodule { + freeformType = lib.types.attrsOf settingsFormat.type; + options = { + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "The Redis host."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 6379; + description = "The Redis port."; + }; + }; + }; + settings = lib.mkOption { + description = '' + Configuration for Misskey, see + [`example.yml`](https://github.com/sharkey-dev/sharkey/blob/develop/.config/example.yml) + for all supported options. + ''; + type = lib.types.submodule { + freeformType = lib.types.attrsOf settingsFormat.type; + options = { + url = lib.mkOption { + type = lib.types.str; + example = "https://example.tld/"; + description = '' + The final user-facing URL. Do not change after running Misskey for the first time. + + This needs to match up with the configured reverse proxy and is automatically configured when using `services.sharkey.reverseProxy`. + ''; + }; + port = lib.mkOption { + type = lib.types.port; + default = 3000; + description = "The port your Misskey server should listen on."; + }; + socket = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/path/to/sharkey.sock"; + description = "The UNIX socket your Misskey server should listen on."; + }; + chmodSocket = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "777"; + description = "The file access mode of the UNIX socket."; + }; + db = lib.mkOption { + description = "Database settings."; + type = lib.types.submodule { + options = { + host = lib.mkOption { + type = lib.types.str; + default = "/var/run/postgresql"; + example = "localhost"; + description = "The PostgreSQL host."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 5432; + description = "The PostgreSQL port."; + }; + db = lib.mkOption { + type = lib.types.str; + default = "sharkey"; + description = "The database name."; + }; + user = lib.mkOption { + type = lib.types.str; + default = "sharkey"; + description = "The user used for database authentication."; + }; + pass = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The password used for database authentication."; + }; + disableCache = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to disable caching queries."; + }; + extra = lib.mkOption { + type = lib.types.nullOr (lib.types.attrsOf settingsFormat.type); + default = null; + example = { + ssl = true; + }; + description = "Extra connection options."; + }; + }; + }; + default = { }; + }; + redis = lib.mkOption { + type = redisType; + default = { }; + description = "`ioredis` options. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference."; + }; + redisForPubsub = lib.mkOption { + type = lib.types.nullOr redisType; + default = null; + description = "`ioredis` options for pubsub. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference."; + }; + redisForJobQueue = lib.mkOption { + type = lib.types.nullOr redisType; + default = null; + description = "`ioredis` options for the job queue. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference."; + }; + redisForTimelines = lib.mkOption { + type = lib.types.nullOr redisType; + default = null; + description = "`ioredis` options for timelines. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference."; + }; + meilisearch = lib.mkOption { + description = "Meilisearch connection options."; + type = lib.types.nullOr ( + lib.types.submodule { + options = { + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "The Meilisearch host."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 7700; + description = "The Meilisearch port."; + }; + apiKey = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The Meilisearch API key."; + }; + ssl = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to connect via SSL."; + }; + index = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Meilisearch index to use."; + }; + scope = lib.mkOption { + type = lib.types.enum [ + "local" + "global" + ]; + default = "local"; + description = "The search scope."; + }; + }; + } + ); + default = null; + }; + id = lib.mkOption { + type = lib.types.enum [ + "aid" + "aidx" + "meid" + "ulid" + "objectid" + ]; + default = "aidx"; + description = "The ID generation method to use. Do not change after starting Misskey for the first time."; + }; + }; + }; + }; +in + +{ + options = { + services.sharkey = { + enable = lib.mkEnableOption "sharkey"; + package = lib.mkPackageOption pkgs "sharkey" { }; + inherit settings; + database = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Create the PostgreSQL database locally. Sets `services.sharkey.settings.db.{db,host,port,user,pass}`."; + }; + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "The path to a file containing the database password. Sets `services.sharkey.settings.db.pass`."; + }; + }; + redis = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Create and use a local Redis instance. Sets `services.sharkey.settings.redis.host`."; + }; + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "The path to a file containing the Redis password. Sets `services.sharkey.settings.redis.pass`."; + }; + }; + meilisearch = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Create and use a local Meilisearch instance. Sets `services.sharkey.settings.meilisearch.{host,port,ssl}`."; + }; + keyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "The path to a file containing the Meilisearch API key. Sets `services.sharkey.settings.meilisearch.apiKey`."; + }; + }; + reverseProxy = { + enable = lib.mkEnableOption "a HTTP reverse proxy for Misskey"; + webserver = lib.mkOption { + type = lib.types.attrTag { + nginx = lib.mkOption { + type = lib.types.submodule (import ../web-servers/nginx/vhost-options.nix); + default = { }; + description = '' + Extra configuration for the nginx virtual host of Misskey. + Set to `{ }` to use the default configuration. + ''; + }; + caddy = lib.mkOption { + type = lib.types.submodule ( + import ../web-servers/caddy/vhost-options.nix { cfg = config.services.caddy; } + ); + default = { }; + description = '' + Extra configuration for the caddy virtual host of Misskey. + Set to `{ }` to use the default configuration. + ''; + }; + }; + description = "The webserver to use as the reverse proxy."; + }; + host = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = '' + The fully qualified domain name to bind to. Sets `services.sharkey.settings.url`. + + This is required when using `services.sharkey.reverseProxy.enable = true`. + ''; + example = "sharkey.example.com"; + default = null; + }; + ssl = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + description = '' + Whether to enable SSL for the reverse proxy. Sets `services.sharkey.settings.url`. + + This is required when using `services.sharkey.reverseProxy.enable = true`. + ''; + example = true; + default = null; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = + cfg.reverseProxy.enable -> ((cfg.reverseProxy.host != null) && (cfg.reverseProxy.ssl != null)); + message = "`services.sharkey.reverseProxy.enable` requires `services.sharkey.reverseProxy.host` and `services.sharkey.reverseProxy.ssl` to be set."; + } + ]; + + services.sharkey.settings = lib.mkMerge [ + (lib.mkIf cfg.database.createLocally { + db = { + db = lib.mkDefault "sharkey"; + # Use unix socket instead of localhost to allow PostgreSQL peer authentication, + # required for `services.postgresql.ensureUsers` + host = lib.mkDefault "/var/run/postgresql"; + port = lib.mkDefault config.services.postgresql.settings.port; + user = lib.mkDefault "sharkey"; + pass = lib.mkDefault null; + }; + }) + (lib.mkIf (cfg.database.passwordFile != null) { db.pass = lib.mkDefault "@DATABASE_PASSWORD@"; }) + (lib.mkIf cfg.redis.createLocally { redis.host = lib.mkDefault "localhost"; }) + (lib.mkIf (cfg.redis.passwordFile != null) { redis.pass = lib.mkDefault "@REDIS_PASSWORD@"; }) + (lib.mkIf cfg.meilisearch.createLocally { + meilisearch = { + host = lib.mkDefault "localhost"; + port = lib.mkDefault config.services.meilisearch.listenPort; + ssl = lib.mkDefault false; + }; + }) + (lib.mkIf (cfg.meilisearch.keyFile != null) { + meilisearch.apiKey = lib.mkDefault "@MEILISEARCH_KEY@"; + }) + (lib.mkIf cfg.reverseProxy.enable { + url = lib.mkDefault "${ + if cfg.reverseProxy.ssl then "https" else "http" + }://${cfg.reverseProxy.host}"; + }) + ]; + + systemd.services.sharkey = { + after = [ + "network-online.target" + "postgresql.service" + ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + MISSKEY_CONFIG_YML = "/run/sharkey/default.yml"; + }; + preStart = + '' + install -m 700 ${settingsFormat.generate "sharkey-config.yml" cfg.settings} /run/sharkey/default.yml + '' + + (lib.optionalString (cfg.database.passwordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@DATABASE_PASSWORD@' "${cfg.database.passwordFile}" /run/sharkey/default.yml + '') + + (lib.optionalString (cfg.redis.passwordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@REDIS_PASSWORD@' "${cfg.redis.passwordFile}" /run/sharkey/default.yml + '') + + (lib.optionalString (cfg.meilisearch.keyFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@MEILISEARCH_KEY@' "${cfg.meilisearch.keyFile}" /run/sharkey/default.yml + ''); + serviceConfig = { + ExecStart = "${cfg.package}/bin/sharkey migrateandstart"; + RuntimeDirectory = "sharkey"; + RuntimeDirectoryMode = "700"; + StateDirectory = "sharkey"; + StateDirectoryMode = "700"; + TimeoutSec = 60; + DynamicUser = true; + User = "sharkey"; + LockPersonality = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectProc = "invisible"; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK"; + }; + }; + + services.postgresql = lib.mkIf cfg.database.createLocally { + enable = true; + ensureDatabases = [ "sharkey" ]; + ensureUsers = [ + { + name = "sharkey"; + ensureDBOwnership = true; + } + ]; + }; + + services.redis.servers = lib.mkIf cfg.redis.createLocally { + sharkey = { + enable = true; + port = cfg.settings.redis.port; + }; + }; + + services.meilisearch = lib.mkIf cfg.meilisearch.createLocally { enable = true; }; + + services.caddy = lib.mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.webserver ? caddy) { + enable = true; + virtualHosts.${cfg.settings.url} = lib.mkMerge [ + cfg.reverseProxy.webserver.caddy + { + hostName = lib.mkDefault cfg.settings.url; + extraConfig = '' + reverse_proxy localhost:${toString cfg.settings.port} + ''; + } + ]; + }; + + services.nginx = lib.mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.webserver ? nginx) { + enable = true; + virtualHosts.${cfg.reverseProxy.host} = lib.mkMerge [ + cfg.reverseProxy.webserver.nginx + { + locations."/" = { + proxyPass = lib.mkDefault "http://localhost:${toString cfg.settings.port}"; + proxyWebsockets = lib.mkDefault true; + recommendedProxySettings = lib.mkDefault true; + }; + } + (lib.mkIf (cfg.reverseProxy.ssl != null) { forceSSL = lib.mkDefault cfg.reverseProxy.ssl; }) + ]; + }; + }; + + meta = { + maintainers = [ lib.maintainers.feathecutie ]; + }; +} diff --git a/sharkey.nix b/sharkey.nix new file mode 100644 index 0000000000..aca87ca223 --- /dev/null +++ b/sharkey.nix @@ -0,0 +1,124 @@ +{ + stdenv, + lib, + nixosTests, + fetchFromGitHub, + nodejs, + pnpm, + makeWrapper, + python3, + bash, + jemalloc, + ffmpeg-headless, + writeShellScript, + xcbuild, + ... +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "sharkey"; + + version = "2024.10.0"; + + src = fetchGit { + url = "https://activitypub.software/TransFem-org/Sharkey.git"; + rev = "150d949a3ec2b5162e2dfda10c2cc5dddea8c59a"; + submodules = true; + }; + + nativeBuildInputs = [ + nodejs + pnpm.configHook + makeWrapper + python3 + ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ xcbuild.xcrun ]; + + # https://nixos.org/manual/nixpkgs/unstable/#javascript-pnpm + pnpmDeps = pnpm.fetchDeps { + inherit (finalAttrs) pname version src; + hash = "sha256-PpXmNBO4pWj8AG0we4DpPhzfx/18rwDZHi86+esFghM="; + }; + + buildPhase = '' + runHook preBuild + + # https://github.com/NixOS/nixpkgs/pull/296697/files#r1617546739 + ( + cd node_modules/.pnpm/node_modules/v-code-diff + pnpm run postinstall + ) + + # https://github.com/NixOS/nixpkgs/pull/296697/files#r1617595593 + export npm_config_nodedir=${nodejs} + ( + cd node_modules/.pnpm/node_modules/re2 + pnpm run rebuild + ) + ( + cd node_modules/.pnpm/node_modules/sharp + pnpm run install + ) + + pnpm build + + runHook postBuild + ''; + + installPhase = + let + checkEnvVarScript = writeShellScript "sharkey-check-env-var" '' + if [[ -z $MISSKEY_CONFIG_YML ]]; then + echo "MISSKEY_CONFIG_YML must be set to the location of the Misskey config file." + exit 1 + fi + ''; + in + '' + runHook preInstall + + mkdir -p $out/data + cp -r . $out/data + + # Set up symlink for use at runtime + # TODO: Find a better solution for this (potentially patch Misskey to make this configurable?) + # Line that would need to be patched: https://github.com/sharkey-dev/sharkey/blob/9849aab40283cbde2184e74d4795aec8ef8ccba3/packages/backend/src/core/InternalStorageService.ts#L18 + # Otherwise, maybe somehow bindmount a writable directory into /data/files. + ln -s /var/lib/sharkey $out/data/files + + makeWrapper ${pnpm}/bin/pnpm $out/bin/sharkey \ + --run "${checkEnvVarScript} || exit" \ + --chdir $out/data \ + --add-flags run \ + --set-default NODE_ENV production \ + --prefix PATH : ${ + lib.makeBinPath [ + nodejs + pnpm + bash + ] + } \ + --prefix LD_LIBRARY_PATH : ${ + lib.makeLibraryPath [ + jemalloc + ffmpeg-headless + stdenv.cc.cc + ] + } + + runHook postInstall + ''; + + passthru = { + inherit (finalAttrs) pnpmDeps; + tests.sharkey = nixosTests.sharkey; + }; + + meta = { + description = "🌎 An interplanetary microblogging platform 🚀"; + homepage = "https://sharkey-hub.net/"; + license = lib.licenses.agpl3Only; + maintainers = [ lib.maintainers.feathecutie ]; + platforms = lib.platforms.unix; + mainProgram = "sharkey"; + }; +})