summaryrefslogtreecommitdiff
path: root/packages/backend
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend')
-rw-r--r--packages/backend/migration/1683847157541-UserList.js13
-rw-r--r--packages/backend/migration/1683869758873-UserListFavorites.js19
-rw-r--r--packages/backend/migration/1684206886988-remove-showTimelineReplies.js11
-rw-r--r--packages/backend/migration/1684386446061-emoji-improve.js15
-rw-r--r--packages/backend/package.json91
-rw-r--r--packages/backend/src/GlobalModule.ts16
-rw-r--r--packages/backend/src/config.ts6
-rw-r--r--packages/backend/src/core/AntennaService.ts15
-rw-r--r--packages/backend/src/core/CacheService.ts7
-rw-r--r--packages/backend/src/core/CaptchaService.ts20
-rw-r--r--packages/backend/src/core/CustomEmojiService.ts22
-rw-r--r--packages/backend/src/core/FetchInstanceMetadataService.ts6
-rw-r--r--packages/backend/src/core/MetaService.ts7
-rw-r--r--packages/backend/src/core/MfmService.ts2
-rw-r--r--packages/backend/src/core/NoteCreateService.ts10
-rw-r--r--packages/backend/src/core/NoteReadService.ts8
-rw-r--r--packages/backend/src/core/NotificationService.ts8
-rw-r--r--packages/backend/src/core/QueryService.ts4
-rw-r--r--packages/backend/src/core/QueueModule.ts59
-rw-r--r--packages/backend/src/core/QueueService.ts65
-rw-r--r--packages/backend/src/core/ReactionService.ts63
-rw-r--r--packages/backend/src/core/RoleService.ts15
-rw-r--r--packages/backend/src/core/WebfingerService.ts7
-rw-r--r--packages/backend/src/core/WebhookService.ts7
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts6
-rw-r--r--packages/backend/src/core/activitypub/LdSignatureService.ts4
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts6
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts7
-rw-r--r--packages/backend/src/core/chart/ChartManagementService.ts8
-rw-r--r--packages/backend/src/core/entities/EmojiEntityService.ts5
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts1
-rw-r--r--packages/backend/src/core/entities/UserListEntityService.ts1
-rw-r--r--packages/backend/src/daemons/JanitorService.ts7
-rw-r--r--packages/backend/src/daemons/QueueStatsService.ts25
-rw-r--r--packages/backend/src/daemons/ServerStatsService.ts7
-rw-r--r--packages/backend/src/di-symbols.ts1
-rw-r--r--packages/backend/src/misc/id/aid.ts2
-rw-r--r--packages/backend/src/misc/prelude/time.ts17
-rw-r--r--packages/backend/src/models/RepositoryModule.ts10
-rw-r--r--packages/backend/src/models/entities/Emoji.ts16
-rw-r--r--packages/backend/src/models/entities/Note.ts2
-rw-r--r--packages/backend/src/models/entities/User.ts6
-rw-r--r--packages/backend/src/models/entities/UserList.ts6
-rw-r--r--packages/backend/src/models/entities/UserListFavorite.ts33
-rw-r--r--packages/backend/src/models/index.ts3
-rw-r--r--packages/backend/src/models/json-schema/emoji.ts30
-rw-r--r--packages/backend/src/models/json-schema/user-list.ts5
-rw-r--r--packages/backend/src/postgres.ts2
-rw-r--r--packages/backend/src/queue/QueueProcessorService.ts356
-rw-r--r--packages/backend/src/queue/const.ts26
-rw-r--r--packages/backend/src/queue/get-job-info.ts15
-rw-r--r--packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts6
-rw-r--r--packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts5
-rw-r--r--packages/backend/src/queue/processors/CleanChartsProcessorService.ts5
-rw-r--r--packages/backend/src/queue/processors/CleanProcessorService.ts5
-rw-r--r--packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts11
-rw-r--r--packages/backend/src/queue/processors/DeleteAccountProcessorService.ts4
-rw-r--r--packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts12
-rw-r--r--packages/backend/src/queue/processors/DeleteFileProcessorService.ts4
-rw-r--r--packages/backend/src/queue/processors/DeliverProcessorService.ts10
-rw-r--r--packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts7
-rw-r--r--packages/backend/src/queue/processors/ExportAntennasProcessorService.ts6
-rw-r--r--packages/backend/src/queue/processors/ExportBlockingProcessorService.ts13
-rw-r--r--packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts39
-rw-r--r--packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts11
-rw-r--r--packages/backend/src/queue/processors/ExportFollowingProcessorService.ts9
-rw-r--r--packages/backend/src/queue/processors/ExportMutingProcessorService.ts13
-rw-r--r--packages/backend/src/queue/processors/ExportNotesProcessorService.ts11
-rw-r--r--packages/backend/src/queue/processors/ExportUserListsProcessorService.ts9
-rw-r--r--packages/backend/src/queue/processors/ImportAntennasProcessorService.ts6
-rw-r--r--packages/backend/src/queue/processors/ImportBlockingProcessorService.ts13
-rw-r--r--packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts9
-rw-r--r--packages/backend/src/queue/processors/ImportFollowingProcessorService.ts13
-rw-r--r--packages/backend/src/queue/processors/ImportMutingProcessorService.ts11
-rw-r--r--packages/backend/src/queue/processors/ImportUserListsProcessorService.ts7
-rw-r--r--packages/backend/src/queue/processors/InboxProcessorService.ts38
-rw-r--r--packages/backend/src/queue/processors/RelationshipProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/ResyncChartsProcessorService.ts5
-rw-r--r--packages/backend/src/queue/processors/TickChartsProcessorService.ts5
-rw-r--r--packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts6
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts2
-rw-r--r--packages/backend/src/server/ServerService.ts11
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts7
-rw-r--r--packages/backend/src/server/api/AuthenticateService.ts2
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts12
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts136
-rw-r--r--packages/backend/src/server/api/endpoints.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/update.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/admin/relays/add.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/auth/accept.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/apps.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/meta.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/reset-db.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/roles/notes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts148
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/favorite.ts70
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/list.ts45
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/show.ts35
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts63
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/update.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts5
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts11
-rw-r--r--packages/backend/src/server/api/stream/index.ts23
-rw-r--r--packages/backend/src/server/web/boot.js76
-rw-r--r--packages/backend/src/server/web/views/base.pug4
-rw-r--r--packages/backend/src/server/web/views/channel.pug1
-rw-r--r--packages/backend/src/server/web/views/clip.pug1
-rw-r--r--packages/backend/src/server/web/views/flash.pug1
-rw-r--r--packages/backend/src/server/web/views/gallery-post.pug1
-rw-r--r--packages/backend/src/server/web/views/note.pug16
-rw-r--r--packages/backend/src/server/web/views/page.pug1
-rw-r--r--packages/backend/src/server/web/views/user.pug1
-rw-r--r--packages/backend/test/e2e/2fa.ts2
-rw-r--r--packages/backend/test/e2e/antennas.ts653
-rw-r--r--packages/backend/test/e2e/users.ts5
-rw-r--r--packages/backend/test/misc/mock-resolver.ts6
-rw-r--r--packages/backend/test/unit/ReactionService.ts42
-rw-r--r--packages/backend/test/utils.ts99
133 files changed, 2311 insertions, 729 deletions
diff --git a/packages/backend/migration/1683847157541-UserList.js b/packages/backend/migration/1683847157541-UserList.js
new file mode 100644
index 0000000000..b50a50eed8
--- /dev/null
+++ b/packages/backend/migration/1683847157541-UserList.js
@@ -0,0 +1,13 @@
+export class UserList1683847157541 {
+ name = 'UserList1683847157541'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_list" ADD "isPublic" boolean NOT NULL DEFAULT false`);
+ await queryRunner.query(`CREATE INDEX "IDX_48a00f08598662b9ca540521eb" ON "user_list" ("isPublic") `);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`DROP INDEX "public"."IDX_48a00f08598662b9ca540521eb"`);
+ await queryRunner.query(`ALTER TABLE "user_list" DROP COLUMN "isPublic"`);
+ }
+}
diff --git a/packages/backend/migration/1683869758873-UserListFavorites.js b/packages/backend/migration/1683869758873-UserListFavorites.js
new file mode 100644
index 0000000000..ac9c4c42b9
--- /dev/null
+++ b/packages/backend/migration/1683869758873-UserListFavorites.js
@@ -0,0 +1,19 @@
+export class UserListFavorites1683869758873 {
+ name = 'UserListFavorites1683869758873'
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE TABLE "user_list_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userListId" character varying(32) NOT NULL, CONSTRAINT "PK_c0974b21e18502a4c8178e09fe6" PRIMARY KEY ("id"))`);
+ await queryRunner.query(`CREATE INDEX "IDX_016f613dc4feb807e03e3e7da9" ON "user_list_favorite" ("userId") `);
+ await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d6765a8c2a4c17c33f9d7f948b" ON "user_list_favorite" ("userId", "userListId") `);
+ await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD CONSTRAINT "FK_016f613dc4feb807e03e3e7da92" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+ await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD CONSTRAINT "FK_4d52b20bfe32c8552e7a61e80d2" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP CONSTRAINT "FK_4d52b20bfe32c8552e7a61e80d2"`);
+ await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP CONSTRAINT "FK_016f613dc4feb807e03e3e7da92"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d6765a8c2a4c17c33f9d7f948b"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_016f613dc4feb807e03e3e7da9"`);
+ await queryRunner.query(`DROP TABLE "user_list_favorite"`);
+ }
+}
diff --git a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js
new file mode 100644
index 0000000000..690653bd7c
--- /dev/null
+++ b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js
@@ -0,0 +1,11 @@
+export class RemoveShowTimelineReplies1684206886988 {
+ name = 'RemoveShowTimelineReplies1684206886988'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showTimelineReplies"`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user" ADD "showTimelineReplies" boolean NOT NULL DEFAULT false`);
+ }
+}
diff --git a/packages/backend/migration/1684386446061-emoji-improve.js b/packages/backend/migration/1684386446061-emoji-improve.js
new file mode 100644
index 0000000000..40b0a2bc5e
--- /dev/null
+++ b/packages/backend/migration/1684386446061-emoji-improve.js
@@ -0,0 +1,15 @@
+export class EmojiImprove1684386446061 {
+ name = 'EmojiImprove1684386446061'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "emoji" ADD "localOnly" boolean NOT NULL DEFAULT false`);
+ await queryRunner.query(`ALTER TABLE "emoji" ADD "isSensitive" boolean NOT NULL DEFAULT false`);
+ await queryRunner.query(`ALTER TABLE "emoji" ADD "roleIdsThatCanBeUsedThisEmojiAsReaction" character varying(128) array NOT NULL DEFAULT '{}'`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "roleIdsThatCanBeUsedThisEmojiAsReaction"`);
+ await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "isSensitive"`);
+ await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "localOnly"`);
+ }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 4bab4a7341..56ecbc2eaf 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -35,49 +35,52 @@
"@swc/core-win32-x64-msvc": "1.3.56",
"@tensorflow/tfjs": "4.4.0",
"@tensorflow/tfjs-node": "4.4.0",
- "slacc-android-arm-eabi": "0.0.7",
- "slacc-android-arm64": "0.0.7",
- "slacc-darwin-arm64": "0.0.7",
- "slacc-darwin-universal": "0.0.7",
- "slacc-darwin-x64": "0.0.7",
- "slacc-linux-arm-gnueabihf": "0.0.7",
- "slacc-linux-arm64-gnu": "0.0.7",
- "slacc-linux-arm64-musl": "0.0.7",
- "slacc-linux-x64-gnu": "0.0.7",
- "slacc-win32-arm64-msvc": "0.0.7",
- "slacc-win32-x64-msvc": "0.0.7"
+ "bufferutil": "^4.0.7",
+ "slacc-android-arm-eabi": "0.0.9",
+ "slacc-android-arm64": "0.0.9",
+ "slacc-darwin-arm64": "0.0.9",
+ "slacc-darwin-universal": "0.0.9",
+ "slacc-darwin-x64": "0.0.9",
+ "slacc-freebsd-x64": "0.0.9",
+ "slacc-linux-arm-gnueabihf": "0.0.9",
+ "slacc-linux-arm64-gnu": "0.0.9",
+ "slacc-linux-arm64-musl": "0.0.9",
+ "slacc-linux-x64-gnu": "0.0.9",
+ "slacc-win32-arm64-msvc": "0.0.9",
+ "slacc-win32-x64-msvc": "0.0.9",
+ "utf-8-validate": "^6.0.3"
},
"dependencies": {
"@aws-sdk/client-s3": "3.321.1",
"@aws-sdk/lib-storage": "3.321.1",
"@aws-sdk/node-http-handler": "3.321.1",
- "@bull-board/api": "5.1.2",
- "@bull-board/fastify": "5.1.2",
- "@bull-board/ui": "5.1.2",
+ "@bull-board/api": "5.2.0",
+ "@bull-board/fastify": "5.2.0",
+ "@bull-board/ui": "5.2.0",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
- "@fastify/cors": "8.2.1",
+ "@fastify/cors": "8.3.0",
"@fastify/http-proxy": "9.1.0",
"@fastify/multipart": "7.6.0",
- "@fastify/static": "6.10.1",
+ "@fastify/static": "6.10.2",
"@fastify/view": "7.4.1",
- "@nestjs/common": "9.4.0",
- "@nestjs/core": "9.4.0",
- "@nestjs/testing": "9.4.0",
+ "@nestjs/common": "9.4.2",
+ "@nestjs/core": "9.4.2",
+ "@nestjs/testing": "9.4.2",
"@peertube/http-signature": "1.7.0",
- "@sinonjs/fake-timers": "10.0.2",
+ "@sinonjs/fake-timers": "10.2.0",
"@swc/cli": "0.1.62",
- "@swc/core": "1.3.56",
+ "@swc/core": "1.3.61",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
- "bull": "4.10.4",
+ "bullmq": "3.15.0",
"cacheable-lookup": "6.1.0",
- "cbor": "8.1.0",
+ "cbor": "9.0.0",
"chalk": "5.2.0",
"chalk-template": "0.4.0",
"chokidar": "3.5.3",
@@ -93,30 +96,30 @@
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "12.6.0",
- "happy-dom": "9.16.0",
+ "happy-dom": "9.20.3",
"hpagent": "1.2.0",
"ioredis": "5.3.2",
"ip-cidr": "3.1.0",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
- "jsdom": "21.1.1",
+ "jsdom": "22.1.0",
"json5": "2.2.3",
- "jsonld": "8.1.1",
- "meilisearch": "0.32.3",
+ "jsonld": "8.2.0",
"jsrsasign": "10.8.6",
+ "meilisearch": "0.32.5",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"ms": "3.0.0-canary.1",
"nested-property": "4.0.0",
"node-fetch": "3.3.1",
- "nodemailer": "6.9.2",
+ "nodemailer": "6.9.3",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"os-utils": "0.0.14",
"otpauth": "9.1.2",
"parse5": "7.1.2",
- "pg": "8.10.0",
+ "pg": "8.11.0",
"private-ip": "3.0.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@@ -126,7 +129,7 @@
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
- "re2": "1.18.0",
+ "re2": "1.19.0",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
@@ -136,27 +139,26 @@
"s-age": "1.1.2",
"sanitize-html": "2.10.0",
"seedrandom": "3.0.5",
- "semver": "7.5.0",
+ "semver": "7.5.1",
"sharp": "0.32.1",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
- "slacc": "0.0.7",
+ "slacc": "0.0.9",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
- "systeminformation": "5.17.12",
+ "systeminformation": "5.17.16",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.16",
- "typescript": "5.0.4",
+ "typescript": "5.1.3",
"ulid": "2.3.0",
- "unzipper": "0.10.11",
+ "unzipper": "0.10.14",
"uuid": "9.0.0",
"vary": "1.1.2",
"web-push": "3.6.1",
- "websocket": "1.0.34",
"ws": "8.13.0",
"xev": "3.0.2"
},
@@ -166,23 +168,22 @@
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2",
- "@types/bull": "4.10.0",
"@types/cbor": "6.0.0",
"@types/color-convert": "2.0.0",
"@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21",
- "@types/jest": "29.5.1",
+ "@types/jest": "29.5.2",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
- "@types/node": "20.1.3",
+ "@types/node": "20.2.5",
"@types/node-fetch": "3.0.3",
- "@types/nodemailer": "6.4.7",
+ "@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1",
- "@types/pg": "8.6.6",
+ "@types/pg": "8.10.1",
"@types/pug": "2.0.6",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.5.0",
@@ -196,17 +197,17 @@
"@types/sinonjs__fake-timers": "8.1.2",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
- "@types/unzipper": "0.10.5",
+ "@types/unzipper": "0.10.6",
"@types/uuid": "9.0.1",
"@types/vary": "1.1.0",
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
- "@typescript-eslint/eslint-plugin": "5.59.5",
- "@typescript-eslint/parser": "5.59.5",
+ "@typescript-eslint/eslint-plugin": "5.59.8",
+ "@typescript-eslint/parser": "5.59.8",
"aws-sdk-client-mock": "2.1.1",
"cross-env": "7.0.3",
- "eslint": "8.40.0",
+ "eslint": "8.41.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.5.0",
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 5fb4e8ef3c..406e3192bb 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -4,7 +4,7 @@ import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
import { MeiliSearch } from 'meilisearch';
import { DI } from './di-symbols.js';
-import { loadConfig } from './config.js';
+import { Config, loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
@@ -25,7 +25,7 @@ const $db: Provider = {
const $meilisearch: Provider = {
provide: DI.meilisearch,
- useFactory: (config) => {
+ useFactory: (config: Config) => {
if (config.meilisearch) {
return new MeiliSearch({
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
@@ -40,7 +40,7 @@ const $meilisearch: Provider = {
const $redis: Provider = {
provide: DI.redis,
- useFactory: (config) => {
+ useFactory: (config: Config) => {
return new Redis.Redis({
port: config.redis.port,
host: config.redis.host,
@@ -55,7 +55,7 @@ const $redis: Provider = {
const $redisForPub: Provider = {
provide: DI.redisForPub,
- useFactory: (config) => {
+ useFactory: (config: Config) => {
const redis = new Redis.Redis({
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
@@ -71,7 +71,7 @@ const $redisForPub: Provider = {
const $redisForSub: Provider = {
provide: DI.redisForSub,
- useFactory: (config) => {
+ useFactory: (config: Config) => {
const redis = new Redis.Redis({
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
@@ -100,7 +100,7 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
) {}
- async onApplicationShutdown(signal: string): Promise<void> {
+ public async dispose(): Promise<void> {
if (process.env.NODE_ENV === 'test') {
// XXX:
// Shutting down the existing connections causes errors on Jest as
@@ -116,4 +116,8 @@ export class GlobalModule implements OnApplicationShutdown {
this.redisForSub.disconnect(),
]);
}
+
+ async onApplicationShutdown(signal: string): Promise<void> {
+ await this.dispose();
+ }
}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index c6e1075389..9d1945e4d4 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -144,7 +144,7 @@ export function loadConfig() {
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
const clientManifest = clientManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
- : { 'src/init.ts': { file: 'src/init.ts' } };
+ : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const mixin = {} as Mixin;
@@ -165,7 +165,7 @@ export function loadConfig() {
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
- mixin.clientEntry = clientManifest['src/init.ts'];
+ mixin.clientEntry = clientManifest['src/_boot_.ts'];
mixin.clientManifestExists = clientManifestExists;
const externalMediaProxy = config.mediaProxy ?
@@ -190,6 +190,6 @@ function tryCreateUrl(url: string) {
try {
return new URL(url);
} catch (e) {
- throw `url="${url}" is not a valid URL.`;
+ throw new Error(`url="${url}" is not a valid URL.`);
}
}
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 2d4226a32d..d8df371916 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -56,11 +56,6 @@ export class AntennaService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
- this.redisForSub.off('message', this.onRedisMessage);
- }
-
- @bindThis
private async onRedisMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
@@ -196,4 +191,14 @@ export class AntennaService implements OnApplicationShutdown {
return this.antennas;
}
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onRedisMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts
index cf1e81ffc8..de33e4c243 100644
--- a/packages/backend/src/core/CacheService.ts
+++ b/packages/backend/src/core/CacheService.ts
@@ -166,7 +166,12 @@ export class CacheService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index 7aaa1b833f..1a52a229c5 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -30,7 +30,7 @@ export class CaptchaService {
}, { throwErrorWhenResponseNotOk: false });
if (!res.ok) {
- throw `${res.status}`;
+ throw new Error(`${res.status}`);
}
return await res.json() as CaptchaResponse;
@@ -39,48 +39,48 @@ export class CaptchaService {
@bindThis
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {
- throw 'recaptcha-failed: no response provided';
+ throw new Error('recaptcha-failed: no response provided');
}
const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => {
- throw `recaptcha-request-failed: ${err}`;
+ throw new Error(`recaptcha-request-failed: ${err}`);
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
- throw `recaptcha-failed: ${errorCodes}`;
+ throw new Error(`recaptcha-failed: ${errorCodes}`);
}
}
@bindThis
public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {
- throw 'hcaptcha-failed: no response provided';
+ throw new Error('hcaptcha-failed: no response provided');
}
const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
- throw `hcaptcha-request-failed: ${err}`;
+ throw new Error(`hcaptcha-request-failed: ${err}`);
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
- throw `hcaptcha-failed: ${errorCodes}`;
+ throw new Error(`hcaptcha-failed: ${errorCodes}`);
}
}
@bindThis
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {
- throw 'turnstile-failed: no response provided';
+ throw new Error('turnstile-failed: no response provided');
}
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
- throw `turnstile-request-failed: ${err}`;
+ throw new Error(`turnstile-request-failed: ${err}`);
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
- throw `turnstile-failed: ${errorCodes}`;
+ throw new Error(`turnstile-failed: ${errorCodes}`);
}
}
}
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 93557ce617..3499df38b7 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -7,7 +7,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js';
-import type { EmojisRepository } from '@/models/index.js';
+import type { EmojisRepository, Role } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
@@ -15,6 +15,8 @@ import type { Config } from '@/config.js';
import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/server/api/stream/types.js';
+const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
+
@Injectable()
export class CustomEmojiService {
private cache: MemoryKVCache<Emoji | null>;
@@ -63,6 +65,9 @@ export class CustomEmojiService {
aliases: string[];
host: string | null;
license: string | null;
+ isSensitive: boolean;
+ localOnly: boolean;
+ roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][];
}): Promise<Emoji> {
const emoji = await this.emojisRepository.insert({
id: this.idService.genId(),
@@ -75,6 +80,9 @@ export class CustomEmojiService {
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
type: data.driveFile.webpublicType ?? data.driveFile.type,
license: data.license,
+ isSensitive: data.isSensitive,
+ localOnly: data.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) {
@@ -90,10 +98,14 @@ export class CustomEmojiService {
@bindThis
public async update(id: Emoji['id'], data: {
+ driveFile?: DriveFile;
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
+ isSensitive?: boolean;
+ localOnly?: boolean;
+ roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][];
}): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
@@ -105,6 +117,12 @@ export class CustomEmojiService {
category: data.category,
aliases: data.aliases,
license: data.license,
+ isSensitive: data.isSensitive,
+ localOnly: data.localOnly,
+ originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
+ publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined,
+ type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
});
this.localEmojisCache.refresh();
@@ -259,7 +277,7 @@ export class CustomEmojiService {
@bindThis
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
- const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
+ const match = emojiName.match(parseEmojiStrRegexp);
if (!match) return { name: null, host: null };
const name = match[1];
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index 8103d5afe9..9de633350b 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -116,14 +116,14 @@ export class FetchInstanceMetadataService {
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
.catch(err => {
if (err.statusCode === 404) {
- throw 'No nodeinfo provided';
+ throw new Error('No nodeinfo provided');
} else {
throw err.statusCode ?? err.message;
}
}) as Record<string, unknown>;
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
- throw 'No wellknown links';
+ throw new Error('No wellknown links');
}
const links = wellknown.links as any[];
@@ -134,7 +134,7 @@ export class FetchInstanceMetadataService {
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
if (link == null) {
- throw 'No nodeinfo link provided';
+ throw new Error('No nodeinfo link provided');
}
const info = await this.httpRequestService.getJson(link.href)
diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts
index 0b861be8d0..5acc9ad9ad 100644
--- a/packages/backend/src/core/MetaService.ts
+++ b/packages/backend/src/core/MetaService.ts
@@ -120,8 +120,13 @@ export class MetaService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
clearInterval(this.intervalId);
this.redisForSub.off('message', this.onMessage);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 9b2d5dc0ff..dffee16e08 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -83,7 +83,7 @@ export class MfmService {
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
text += txt;
// メンション
- } else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
+ } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@');
if (part.length === 2 && href) {
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 977c9052c0..1c8491bf57 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -510,7 +510,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.poll && data.poll.expiresAt) {
const delay = data.poll.expiresAt.getTime() - Date.now();
- this.queueService.endedPollNotificationQueue.add({
+ this.queueService.endedPollNotificationQueue.add(note.id, {
noteId: note.id,
}, {
delay,
@@ -790,7 +790,13 @@ export class NoteCreateService implements OnApplicationShutdown {
return mentionedUsers;
}
- onApplicationShutdown(signal?: string | undefined) {
+ @bindThis
+ public dispose(): void {
this.#shutdownController.abort();
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts
index 1129bd159c..e57e57d310 100644
--- a/packages/backend/src/core/NoteReadService.ts
+++ b/packages/backend/src/core/NoteReadService.ts
@@ -122,7 +122,13 @@ export class NoteReadService implements OnApplicationShutdown {
}
}
- onApplicationShutdown(signal?: string | undefined): void {
+ @bindThis
+ public dispose(): void {
this.#shutdownController.abort();
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index a245908c98..ed47165f7b 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -152,7 +152,13 @@ export class NotificationService implements OnApplicationShutdown {
*/
}
- onApplicationShutdown(signal?: string | undefined): void {
+ @bindThis
+ public dispose(): void {
this.#shutdownController.abort();
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts
index 0cee2076bf..bf50a1cded 100644
--- a/packages/backend/src/core/QueryService.ts
+++ b/packages/backend/src/core/QueryService.ts
@@ -208,7 +208,7 @@ export class QueryService {
}
@bindThis
- public generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null): void {
+ public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<User, 'id'> | null): void {
if (me == null) {
q.andWhere(new Brackets(qb => { qb
.where('note.replyId IS NULL') // 返信ではない
@@ -217,7 +217,7 @@ export class QueryService {
.andWhere('note.replyUserId = note.userId');
}));
}));
- } else if (!me.showTimelineReplies) {
+ } else if (!withReplies) {
q.andWhere(new Brackets(qb => { qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts
index 1d73947776..3384ca4577 100644
--- a/packages/backend/src/core/QueueModule.ts
+++ b/packages/backend/src/core/QueueModule.ts
@@ -1,42 +1,11 @@
import { setTimeout } from 'node:timers/promises';
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
-import Bull from 'bull';
+import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
+import { QUEUE, baseQueueOptions } from '@/queue/const.js';
import type { Provider } from '@nestjs/common';
-import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js';
-
-function q<T>(config: Config, name: string, limitPerSec = -1) {
- return new Bull<T>(name, {
- redis: {
- port: config.redisForJobQueue.port,
- host: config.redisForJobQueue.host,
- family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
- password: config.redisForJobQueue.pass,
- db: config.redisForJobQueue.db ?? 0,
- },
- prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
- limiter: limitPerSec > 0 ? {
- max: limitPerSec,
- duration: 1000,
- } : undefined,
- settings: {
- backoffStrategies: {
- apBackoff,
- },
- },
- });
-}
-
-// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
-function apBackoff(attemptsMade: number, err: Error) {
- const baseDelay = 60 * 1000; // 1min
- const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
- let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
- backoff = Math.min(backoff, maxBackoff);
- backoff += Math.round(backoff * Math.random() * 0.2);
- return backoff;
-}
+import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
@@ -49,49 +18,49 @@ export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
const $system: Provider = {
provide: 'queue:system',
- useFactory: (config: Config) => q(config, 'system'),
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM, baseQueueOptions(config, QUEUE.SYSTEM)),
inject: [DI.config],
};
const $endedPollNotification: Provider = {
provide: 'queue:endedPollNotification',
- useFactory: (config: Config) => q(config, 'endedPollNotification'),
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.ENDED_POLL_NOTIFICATION, baseQueueOptions(config, QUEUE.ENDED_POLL_NOTIFICATION)),
inject: [DI.config],
};
const $deliver: Provider = {
provide: 'queue:deliver',
- useFactory: (config: Config) => q(config, 'deliver', config.deliverJobPerSec ?? 128),
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
inject: [DI.config],
};
const $inbox: Provider = {
provide: 'queue:inbox',
- useFactory: (config: Config) => q(config, 'inbox', config.inboxJobPerSec ?? 16),
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config, QUEUE.INBOX)),
inject: [DI.config],
};
const $db: Provider = {
provide: 'queue:db',
- useFactory: (config: Config) => q(config, 'db'),
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.DB, baseQueueOptions(config, QUEUE.DB)),
inject: [DI.config],
};
const $relationship: Provider = {
provide: 'queue:relationship',
- useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64),
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config, QUEUE.RELATIONSHIP)),
inject: [DI.config],
};
const $objectStorage: Provider = {
provide: 'queue:objectStorage',
- useFactory: (config: Config) => q(config, 'objectStorage'),
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.OBJECT_STORAGE, baseQueueOptions(config, QUEUE.OBJECT_STORAGE)),
inject: [DI.config],
};
const $webhookDeliver: Provider = {
provide: 'queue:webhookDeliver',
- useFactory: (config: Config) => q(config, 'webhookDeliver', 64),
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
inject: [DI.config],
};
@@ -131,7 +100,7 @@ export class QueueModule implements OnApplicationShutdown {
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {}
- async onApplicationShutdown(signal: string): Promise<void> {
+ public async dispose(): Promise<void> {
if (process.env.NODE_ENV === 'test') {
// XXX:
// Shutting down the existing connections causes errors on Jest as
@@ -151,4 +120,8 @@ export class QueueModule implements OnApplicationShutdown {
this.webhookDeliverQueue.close(),
]);
}
+
+ async onApplicationShutdown(signal: string): Promise<void> {
+ await this.dispose();
+ }
}
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index b4ffffecc0..2ae8a2b754 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -1,6 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
-import Bull from 'bull';
import type { IActivity } from '@/core/activitypub/type.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
@@ -11,6 +10,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature';
+import type * as Bull from 'bullmq';
@Injectable()
export class QueueService {
@@ -26,7 +26,43 @@ export class QueueService {
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
- ) {}
+ ) {
+ this.systemQueue.add('tickCharts', {
+ }, {
+ repeat: { pattern: '55 * * * *' },
+ removeOnComplete: true,
+ });
+
+ this.systemQueue.add('resyncCharts', {
+ }, {
+ repeat: { pattern: '0 0 * * *' },
+ removeOnComplete: true,
+ });
+
+ this.systemQueue.add('cleanCharts', {
+ }, {
+ repeat: { pattern: '0 0 * * *' },
+ removeOnComplete: true,
+ });
+
+ this.systemQueue.add('aggregateRetention', {
+ }, {
+ repeat: { pattern: '0 0 * * *' },
+ removeOnComplete: true,
+ });
+
+ this.systemQueue.add('clean', {
+ }, {
+ repeat: { pattern: '0 0 * * *' },
+ removeOnComplete: true,
+ });
+
+ this.systemQueue.add('checkExpiredMutings', {
+ }, {
+ repeat: { pattern: '*/5 * * * *' },
+ removeOnComplete: true,
+ });
+ }
@bindThis
public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
@@ -42,11 +78,10 @@ export class QueueService {
isSharedInbox,
};
- return this.deliverQueue.add(data, {
+ return this.deliverQueue.add(to, data, {
attempts: this.config.deliverJobMaxAttempts ?? 12,
- timeout: 1 * 60 * 1000, // 1min
backoff: {
- type: 'apBackoff',
+ type: 'custom',
},
removeOnComplete: true,
removeOnFail: true,
@@ -60,11 +95,10 @@ export class QueueService {
signature,
};
- return this.inboxQueue.add(data, {
+ return this.inboxQueue.add('', data, {
attempts: this.config.inboxJobMaxAttempts ?? 8,
- timeout: 5 * 60 * 1000, // 5min
backoff: {
- type: 'apBackoff',
+ type: 'custom',
},
removeOnComplete: true,
removeOnFail: true,
@@ -212,7 +246,7 @@ export class QueueService {
private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): {
name: string,
data: D,
- opts: Bull.JobOptions,
+ opts: Bull.JobsOptions,
} {
return {
name,
@@ -299,10 +333,10 @@ export class QueueService {
}
@bindThis
- private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): {
+ private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobsOptions = {}): {
name: string,
data: RelationshipJobData,
- opts: Bull.JobOptions,
+ opts: Bull.JobsOptions,
} {
return {
name,
@@ -351,11 +385,10 @@ export class QueueService {
eventId: uuid(),
};
- return this.webhookDeliverQueue.add(data, {
+ return this.webhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
- timeout: 1 * 60 * 1000, // 1min
backoff: {
- type: 'apBackoff',
+ type: 'custom',
},
removeOnComplete: true,
removeOnFail: true,
@@ -367,11 +400,11 @@ export class QueueService {
this.deliverQueue.once('cleaned', (jobs, status) => {
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
- this.deliverQueue.clean(0, 'delayed');
+ this.deliverQueue.clean(0, Infinity, 'delayed');
this.inboxQueue.once('cleaned', (jobs, status) => {
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
- this.inboxQueue.clean(0, 'delayed');
+ this.inboxQueue.clean(0, Infinity, 'delayed');
}
}
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index a274b19e4b..4b01b6af7e 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { RoleService } from '@/core/RoleService.js';
const FALLBACK = '❤';
@@ -54,6 +55,9 @@ type DecodedReaction = {
host?: string | null;
};
+const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/;
+const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
+
@Injectable()
export class ReactionService {
constructor(
@@ -72,6 +76,7 @@ export class ReactionService {
private utilityService: UtilityService,
private metaService: MetaService,
private customEmojiService: CustomEmojiService,
+ private roleService: RoleService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
@@ -85,7 +90,7 @@ export class ReactionService {
}
@bindThis
- public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) {
+ public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, _reaction?: string | null) {
// Check blocking
if (note.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@@ -99,10 +104,41 @@ export class ReactionService {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
}
- if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
+ let reaction = _reaction ?? FALLBACK;
+
+ if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '❤️';
- } else {
- reaction = await this.toDbReaction(reaction, user.host);
+ } else if (_reaction) {
+ const custom = reaction.match(isCustomEmojiRegexp);
+ if (custom) {
+ const reacterHost = this.utilityService.toPunyNullable(user.host);
+
+ const name = custom[1];
+ const emoji = reacterHost == null
+ ? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
+ : await this.emojisRepository.findOneBy({
+ host: reacterHost,
+ name,
+ });
+
+ if (emoji) {
+ if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) {
+ reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
+
+ // センシティブ
+ if ((note.reactionAcceptance === 'nonSensitiveOnly') && emoji.isSensitive) {
+ reaction = FALLBACK;
+ }
+ } else {
+ // リアクションとして使う権限がない
+ reaction = FALLBACK;
+ }
+ } else {
+ reaction = FALLBACK;
+ }
+ } else {
+ reaction = this.normalize(reaction ?? null);
+ }
}
const record: NoteReaction = {
@@ -288,11 +324,9 @@ export class ReactionService {
}
@bindThis
- public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
+ public normalize(reaction: string | null): string {
if (reaction == null) return FALLBACK;
- reacterHost = this.utilityService.toPunyNullable(reacterHost);
-
// 文字列タイプのリアクションを絵文字に変換
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
@@ -306,25 +340,12 @@ export class ReactionService {
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
}
- const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
- if (custom) {
- const name = custom[1];
- const emoji = reacterHost == null
- ? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
- : await this.emojisRepository.findOneBy({
- host: reacterHost,
- name,
- });
-
- if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
- }
-
return FALLBACK;
}
@bindThis
public decodeReaction(str: string): DecodedReaction {
- const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
+ const custom = str.match(decodeCustomEmojiRegexp);
if (custom) {
const name = custom[1];
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 68087ccc3b..40ae106662 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -307,6 +307,14 @@ export class RoleService implements OnApplicationShutdown {
}
@bindThis
+ public async isExplorable(role: { id: Role['id']} | null): Promise<boolean> {
+ if (role == null) return false;
+ const check = await this.rolesRepository.findOneBy({ id: role.id });
+ if (check == null) return false;
+ return check.isExplorable;
+ }
+
+ @bindThis
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
@@ -425,7 +433,12 @@ export class RoleService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts
index 3ee7990643..f58a6a10fc 100644
--- a/packages/backend/src/core/WebfingerService.ts
+++ b/packages/backend/src/core/WebfingerService.ts
@@ -16,6 +16,9 @@ type IWebFinger = {
subject: string;
};
+const urlRegex = /^https?:\/\//;
+const mRegex = /^([^@]+)@(.*)/;
+
@Injectable()
export class WebfingerService {
constructor(
@@ -35,12 +38,12 @@ export class WebfingerService {
@bindThis
private genUrl(query: string): string {
- if (query.match(/^https?:\/\//)) {
+ if (query.match(urlRegex)) {
const u = new URL(query);
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
}
- const m = query.match(/^([^@]+)@(.*)/);
+ const m = query.match(mRegex);
if (m) {
const hostname = m[2];
const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
index 57baade777..467755a072 100644
--- a/packages/backend/src/core/WebhookService.ts
+++ b/packages/backend/src/core/WebhookService.ts
@@ -81,7 +81,12 @@ export class WebhookService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 60e19bfca5..d8b95ca4d1 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -277,7 +277,7 @@ export class ApRendererService {
const name = reaction.replaceAll(':', '');
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
- if (emoji) object.tag = [this.renderEmoji(emoji)];
+ if (emoji && !emoji.localOnly) object.tag = [this.renderEmoji(emoji)];
}
return object;
@@ -400,7 +400,7 @@ export class ApRendererService {
}));
const emojis = await this.getEmojis(note.emojis);
- const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
+ const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
const tag = [
...hashtagTags,
@@ -479,7 +479,7 @@ export class ApRendererService {
}
const emojis = await this.getEmojis(user.emojis);
- const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
+ const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag));
diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts
index 2dc1a410ac..20fe2a0a77 100644
--- a/packages/backend/src/core/activitypub/LdSignatureService.ts
+++ b/packages/backend/src/core/activitypub/LdSignatureService.ts
@@ -94,7 +94,7 @@ class LdSignature {
@bindThis
private getLoader() {
return async (url: string): Promise<any> => {
- if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`;
+ if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`);
if (this.preLoad) {
if (url in CONTEXTS) {
@@ -126,7 +126,7 @@ class LdSignature {
timeout: this.loderTimeout,
}, { throwErrorWhenResponseNotOk: false }).then(res => {
if (!res.ok) {
- throw `${res.status} ${res.statusText}`;
+ throw new Error(`${res.status} ${res.statusText}`);
} else {
return res.json();
}
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 87a9db405f..76757f530a 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -18,6 +18,7 @@ import { PollService } from '@/core/PollService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
+import { checkHttps } from '@/misc/check-https.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js';
@@ -32,7 +33,6 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js';
-import { checkHttps } from '@/misc/check-https.js';
@Injectable()
export class ApNoteService {
@@ -230,7 +230,7 @@ export class ApNoteService {
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
if (!quote) {
if (results.some(x => x.status === 'temperror')) {
- throw 'quote resolve failed';
+ throw new Error('quote resolve failed');
}
}
}
@@ -311,7 +311,7 @@ export class ApNoteService {
// ブロックしてたら中断
const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw { statusCode: 451 };
+ if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
const unlock = await this.appLockService.getApLock(uri);
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index eea1d1b848..f52ebed107 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -32,6 +32,8 @@ import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import type { AccountMoveService } from '@/core/AccountMoveService.js';
+import { checkHttps } from '@/misc/check-https.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -42,8 +44,6 @@ import type { ApLoggerService } from '../ApLoggerService.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { ApImageService } from './ApImageService.js';
import type { IActor, IObject } from '../type.js';
-import type { AccountMoveService } from '@/core/AccountMoveService.js';
-import { checkHttps } from '@/misc/check-https.js';
const nameLength = 128;
const summaryLength = 2048;
@@ -306,7 +306,6 @@ export class ApPersonService implements OnModuleInit {
tags,
isBot,
isCat: (person as any).isCat === true,
- showTimelineReplies: false,
})) as RemoteUser;
await transactionalEntityManager.save(new UserProfile({
@@ -696,7 +695,7 @@ export class ApPersonService implements OnModuleInit {
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
return 'skip: dst.alsoKnownAs is empty';
}
- if (!dst.alsoKnownAs?.includes(src.uri)) {
+ if (!dst.alsoKnownAs.includes(src.uri)) {
return 'skip: alsoKnownAs does not include from.uri';
}
diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts
index 03e3612658..b0e9e534df 100644
--- a/packages/backend/src/core/chart/ChartManagementService.ts
+++ b/packages/backend/src/core/chart/ChartManagementService.ts
@@ -60,7 +60,8 @@ export class ChartManagementService implements OnApplicationShutdown {
}, 1000 * 60 * 20);
}
- async onApplicationShutdown(signal: string): Promise<void> {
+ @bindThis
+ public async dispose(): Promise<void> {
clearInterval(this.saveIntervalId);
if (process.env.NODE_ENV !== 'test') {
await Promise.all(
@@ -68,4 +69,9 @@ export class ChartManagementService implements OnApplicationShutdown {
);
}
}
+
+ @bindThis
+ async onApplicationShutdown(signal: string): Promise<void> {
+ await this.dispose();
+ }
}
diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts
index 3bad048bc0..4a18cd1b3b 100644
--- a/packages/backend/src/core/entities/EmojiEntityService.ts
+++ b/packages/backend/src/core/entities/EmojiEntityService.ts
@@ -26,6 +26,8 @@ export class EmojiEntityService {
category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
+ isSensitive: emoji.isSensitive ? true : undefined,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
};
}
@@ -51,6 +53,9 @@ export class EmojiEntityService {
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
license: emoji.license,
+ isSensitive: emoji.isSensitive,
+ localOnly: emoji.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
};
}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 7f61e1d6f3..bfd506ea86 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -466,7 +466,6 @@ export class UserEntityService implements OnModuleInit {
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes,
- showTimelineReplies: user.showTimelineReplies ?? falsy,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
policies: this.roleService.getUserPolicies(user.id),
diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts
index 2461cb2c12..8628819278 100644
--- a/packages/backend/src/core/entities/UserListEntityService.ts
+++ b/packages/backend/src/core/entities/UserListEntityService.ts
@@ -35,6 +35,7 @@ export class UserListEntityService {
createdAt: userList.createdAt.toISOString(),
name: userList.name,
userIds: users.map(x => x.userId),
+ isPublic: userList.isPublic,
};
}
}
diff --git a/packages/backend/src/daemons/JanitorService.ts b/packages/backend/src/daemons/JanitorService.ts
index 8cdfb703f1..f826d50625 100644
--- a/packages/backend/src/daemons/JanitorService.ts
+++ b/packages/backend/src/daemons/JanitorService.ts
@@ -34,7 +34,12 @@ export class JanitorService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
clearInterval(this.intervalId);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts
index b717434e09..53a0d14cd7 100644
--- a/packages/backend/src/daemons/QueueStatsService.ts
+++ b/packages/backend/src/daemons/QueueStatsService.ts
@@ -1,7 +1,11 @@
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import Xev from 'xev';
+import * as Bull from 'bullmq';
import { QueueService } from '@/core/QueueService.js';
import { bindThis } from '@/decorators.js';
+import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
+import { QUEUE, baseQueueOptions } from '@/queue/const.js';
import type { OnApplicationShutdown } from '@nestjs/common';
const ev = new Xev();
@@ -13,6 +17,9 @@ export class QueueStatsService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer;
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
private queueService: QueueService,
) {
}
@@ -31,11 +38,14 @@ export class QueueStatsService implements OnApplicationShutdown {
let activeDeliverJobs = 0;
let activeInboxJobs = 0;
- this.queueService.deliverQueue.on('global:active', () => {
+ const deliverQueueEvents = new Bull.QueueEvents(QUEUE.DELIVER, baseQueueOptions(this.config, QUEUE.DELIVER));
+ const inboxQueueEvents = new Bull.QueueEvents(QUEUE.INBOX, baseQueueOptions(this.config, QUEUE.INBOX));
+
+ deliverQueueEvents.on('active', () => {
activeDeliverJobs++;
});
- this.queueService.inboxQueue.on('global:active', () => {
+ inboxQueueEvents.on('active', () => {
activeInboxJobs++;
});
@@ -71,9 +81,14 @@ export class QueueStatsService implements OnApplicationShutdown {
this.intervalId = setInterval(tick, interval);
}
-
+
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
clearInterval(this.intervalId);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts
index bb190cf60f..6cd71c0e2a 100644
--- a/packages/backend/src/daemons/ServerStatsService.ts
+++ b/packages/backend/src/daemons/ServerStatsService.ts
@@ -63,9 +63,14 @@ export class ServerStatsService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
clearInterval(this.intervalId);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
// CPU STAT
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index c06c7a7159..4a073f102f 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -25,6 +25,7 @@ export const DI = {
userSecurityKeysRepository: Symbol('userSecurityKeysRepository'),
userPublickeysRepository: Symbol('userPublickeysRepository'),
userListsRepository: Symbol('userListsRepository'),
+ userListFavoritesRepository: Symbol('userListFavoritesRepository'),
userListJoiningsRepository: Symbol('userListJoiningsRepository'),
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
userIpsRepository: Symbol('userIpsRepository'),
diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts
index 9e206ee98f..f0cbc9900d 100644
--- a/packages/backend/src/misc/id/aid.ts
+++ b/packages/backend/src/misc/id/aid.ts
@@ -21,7 +21,7 @@ function getNoise(): string {
export function genAid(date: Date): string {
const t = date.getTime();
- if (isNaN(t)) throw 'Failed to create AID: Invalid Date';
+ if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date');
counter++;
return getTime(t) + getNoise();
}
diff --git a/packages/backend/src/misc/prelude/time.ts b/packages/backend/src/misc/prelude/time.ts
index 34e8b6b17c..b21978b186 100644
--- a/packages/backend/src/misc/prelude/time.ts
+++ b/packages/backend/src/misc/prelude/time.ts
@@ -5,15 +5,16 @@ const dateTimeIntervals = {
};
export function dateUTC(time: number[]): Date {
- const d = time.length === 2 ? Date.UTC(time[0], time[1])
- : time.length === 3 ? Date.UTC(time[0], time[1], time[2])
- : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
- : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
- : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
- : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
- : null;
+ const d =
+ time.length === 2 ? Date.UTC(time[0], time[1])
+ : time.length === 3 ? Date.UTC(time[0], time[1], time[2])
+ : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
+ : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
+ : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
+ : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
+ : null;
- if (!d) throw 'wrong number of arguments';
+ if (!d) throw new Error('wrong number of arguments');
return new Date(d);
}
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 588c98b58d..4231acc046 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo } from './index.js';
+import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite } from './index.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@@ -112,6 +112,12 @@ const $userListsRepository: Provider = {
inject: [DI.db],
};
+const $userListFavoritesRepository: Provider = {
+ provide: DI.userListFavoritesRepository,
+ useFactory: (db: DataSource) => db.getRepository(UserListFavorite),
+ inject: [DI.db],
+};
+
const $userListJoiningsRepository: Provider = {
provide: DI.userListJoiningsRepository,
useFactory: (db: DataSource) => db.getRepository(UserListJoining),
@@ -416,6 +422,7 @@ const $userMemosRepository: Provider = {
$userSecurityKeysRepository,
$userPublickeysRepository,
$userListsRepository,
+ $userListFavoritesRepository,
$userListJoiningsRepository,
$userNotePiningsRepository,
$userIpsRepository,
@@ -483,6 +490,7 @@ const $userMemosRepository: Provider = {
$userSecurityKeysRepository,
$userPublickeysRepository,
$userListsRepository,
+ $userListFavoritesRepository,
$userListJoiningsRepository,
$userNotePiningsRepository,
$userIpsRepository,
diff --git a/packages/backend/src/models/entities/Emoji.ts b/packages/backend/src/models/entities/Emoji.ts
index dbb437d439..8fd3e65f5e 100644
--- a/packages/backend/src/models/entities/Emoji.ts
+++ b/packages/backend/src/models/entities/Emoji.ts
@@ -60,4 +60,20 @@ export class Emoji {
length: 1024, nullable: true,
})
public license: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public localOnly: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isSensitive: boolean;
+
+ // TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
+ @Column('varchar', {
+ array: true, length: 128, default: '{}',
+ })
+ public roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
}
diff --git a/packages/backend/src/models/entities/Note.ts b/packages/backend/src/models/entities/Note.ts
index df508b4dca..4f49a05950 100644
--- a/packages/backend/src/models/entities/Note.ts
+++ b/packages/backend/src/models/entities/Note.ts
@@ -90,7 +90,7 @@ export class Note {
@Column('varchar', {
length: 64, nullable: true,
})
- public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | null;
+ public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
@Column('smallint', {
default: 0,
diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts
index 8e10f999b6..6669890cf6 100644
--- a/packages/backend/src/models/entities/User.ts
+++ b/packages/backend/src/models/entities/User.ts
@@ -232,12 +232,6 @@ export class User {
})
public followersUri: string | null;
- @Column('boolean', {
- default: false,
- comment: 'Whether to show users replying to other users in the timeline.',
- })
- public showTimelineReplies: boolean;
-
@Index({ unique: true })
@Column('char', {
length: 16, nullable: true, unique: true,
diff --git a/packages/backend/src/models/entities/UserList.ts b/packages/backend/src/models/entities/UserList.ts
index b8a4b54d4c..94f3dc3cb3 100644
--- a/packages/backend/src/models/entities/UserList.ts
+++ b/packages/backend/src/models/entities/UserList.ts
@@ -19,6 +19,12 @@ export class UserList {
})
public userId: User['id'];
+ @Index()
+ @Column('boolean', {
+ default: false,
+ })
+ public isPublic: boolean;
+
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
diff --git a/packages/backend/src/models/entities/UserListFavorite.ts b/packages/backend/src/models/entities/UserListFavorite.ts
new file mode 100644
index 0000000000..e57abb460a
--- /dev/null
+++ b/packages/backend/src/models/entities/UserListFavorite.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from '../id.js';
+import { User } from './User.js';
+import { UserList } from './UserList.js';
+
+@Entity()
+@Index(['userId', 'userListId'], { unique: true })
+export class UserListFavorite {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone')
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column(id())
+ public userListId: UserList['id'];
+
+ @ManyToOne(type => UserList, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public userList: UserList | null;
+}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index b8ba28db9b..4b230ab742 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -49,6 +49,7 @@ import { User } from '@/models/entities/User.js';
import { UserIp } from '@/models/entities/UserIp.js';
import { UserKeypair } from '@/models/entities/UserKeypair.js';
import { UserList } from '@/models/entities/UserList.js';
+import { UserListFavorite } from './entities/UserListFavorite.js';
import { UserListJoining } from '@/models/entities/UserListJoining.js';
import { UserNotePining } from '@/models/entities/UserNotePining.js';
import { UserPending } from '@/models/entities/UserPending.js';
@@ -117,6 +118,7 @@ export {
UserIp,
UserKeypair,
UserList,
+ UserListFavorite,
UserListJoining,
UserNotePining,
UserPending,
@@ -184,6 +186,7 @@ export type UsersRepository = Repository<User>;
export type UserIpsRepository = Repository<UserIp>;
export type UserKeypairsRepository = Repository<UserKeypair>;
export type UserListsRepository = Repository<UserList>;
+export type UserListFavoritesRepository = Repository<UserListFavorite>;
export type UserListJoiningsRepository = Repository<UserListJoining>;
export type UserNotePiningsRepository = Repository<UserNotePining>;
export type UserPendingsRepository = Repository<UserPending>;
diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts
index db4fd62cf6..63f56e77cb 100644
--- a/packages/backend/src/models/json-schema/emoji.ts
+++ b/packages/backend/src/models/json-schema/emoji.ts
@@ -22,6 +22,19 @@ export const packedEmojiSimpleSchema = {
type: 'string',
optional: false, nullable: false,
},
+ isSensitive: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: {
+ type: 'array',
+ optional: true, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ },
},
} as const;
@@ -63,5 +76,22 @@ export const packedEmojiDetailedSchema = {
type: 'string',
optional: false, nullable: true,
},
+ isSensitive: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ localOnly: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/user-list.ts b/packages/backend/src/models/json-schema/user-list.ts
index 3ba5dc4a8a..1e620516e4 100644
--- a/packages/backend/src/models/json-schema/user-list.ts
+++ b/packages/backend/src/models/json-schema/user-list.ts
@@ -25,5 +25,10 @@ export const packedUserListSchema = {
format: 'id',
},
},
+ isPublic: {
+ type: 'boolean',
+ nullable: false,
+ optional: false,
+ },
},
} as const;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index f3d404e6c9..488979c409 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -57,6 +57,7 @@ import { User } from '@/models/entities/User.js';
import { UserIp } from '@/models/entities/UserIp.js';
import { UserKeypair } from '@/models/entities/UserKeypair.js';
import { UserList } from '@/models/entities/UserList.js';
+import { UserListFavorite } from '@/models/entities/UserListFavorite.js';
import { UserListJoining } from '@/models/entities/UserListJoining.js';
import { UserNotePining } from '@/models/entities/UserNotePining.js';
import { UserPending } from '@/models/entities/UserPending.js';
@@ -132,6 +133,7 @@ export const entities = [
UserKeypair,
UserPublickey,
UserList,
+ UserListFavorite,
UserListJoining,
UserNotePining,
UserSecurityKey,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index dc025f9889..42f9c1af7d 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -1,10 +1,9 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
+import * as Bull from 'bullmq';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
-import { QueueService } from '@/core/QueueService.js';
import { bindThis } from '@/decorators.js';
-import { getJobInfo } from './get-job-info.js';
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
@@ -35,17 +34,51 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
+import { QUEUE, baseQueueOptions } from './const.js';
+
+// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
+function httpRelatedBackoff(attemptsMade: number) {
+ const baseDelay = 60 * 1000; // 1min
+ const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
+ let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
+ backoff = Math.min(backoff, maxBackoff);
+ backoff += Math.round(backoff * Math.random() * 0.2);
+ return backoff;
+}
+
+function getJobInfo(job: Bull.Job | undefined, increment = false): string {
+ if (job == null) return '-';
+
+ const age = Date.now() - job.timestamp;
+
+ const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
+ : age > 10000 ? `${Math.floor(age / 1000)}s`
+ : `${age}ms`;
+
+ // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
+ const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
+ const maxAttempts = job.opts ? job.opts.attempts : 0;
+
+ return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
+}
@Injectable()
-export class QueueProcessorService {
+export class QueueProcessorService implements OnApplicationShutdown {
private logger: Logger;
+ private systemQueueWorker: Bull.Worker;
+ private dbQueueWorker: Bull.Worker;
+ private deliverQueueWorker: Bull.Worker;
+ private inboxQueueWorker: Bull.Worker;
+ private webhookDeliverQueueWorker: Bull.Worker;
+ private relationshipQueueWorker: Bull.Worker;
+ private objectStorageQueueWorker: Bull.Worker;
+ private endedPollNotificationQueueWorker: Bull.Worker;
constructor(
@Inject(DI.config)
private config: Config,
private queueLoggerService: QueueLoggerService,
- private queueService: QueueService,
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private deliverProcessorService: DeliverProcessorService,
@@ -77,10 +110,7 @@ export class QueueProcessorService {
private cleanProcessorService: CleanProcessorService,
) {
this.logger = this.queueLoggerService.logger;
- }
- @bindThis
- public start() {
function renderError(e: Error): any {
if (e) { // 何故かeがundefinedで来ることがある
return {
@@ -97,146 +127,232 @@ export class QueueProcessorService {
}
}
+ //#region system
+ this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
+ switch (job.name) {
+ case 'tickCharts': return this.tickChartsProcessorService.process();
+ case 'resyncCharts': return this.resyncChartsProcessorService.process();
+ case 'cleanCharts': return this.cleanChartsProcessorService.process();
+ case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
+ case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
+ case 'clean': return this.cleanProcessorService.process();
+ default: throw new Error(`unrecognized job type ${job.name} for system`);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.SYSTEM),
+ autorun: false,
+ });
+
const systemLogger = this.logger.createSubLogger('system');
- const deliverLogger = this.logger.createSubLogger('deliver');
- const webhookLogger = this.logger.createSubLogger('webhook');
- const inboxLogger = this.logger.createSubLogger('inbox');
- const dbLogger = this.logger.createSubLogger('db');
- const relationshipLogger = this.logger.createSubLogger('relationship');
- const objectStorageLogger = this.logger.createSubLogger('objectStorage');
- this.queueService.systemQueue
- .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
+ this.systemQueueWorker
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
- .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
- .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
+ .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
+ //#endregion
+
+ //#region db
+ this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
+ switch (job.name) {
+ case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
+ case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
+ case 'exportNotes': return this.exportNotesProcessorService.process(job);
+ case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
+ case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
+ case 'exportMuting': return this.exportMutingProcessorService.process(job);
+ case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
+ case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
+ case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
+ case 'importFollowing': return this.importFollowingProcessorService.process(job);
+ case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
+ case 'importMuting': return this.importMutingProcessorService.process(job);
+ case 'importBlocking': return this.importBlockingProcessorService.process(job);
+ case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
+ case 'importUserLists': return this.importUserListsProcessorService.process(job);
+ case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
+ case 'importAntennas': return this.importAntennasProcessorService.process(job);
+ case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
+ default: throw new Error(`unrecognized job type ${job.name} for db`);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.DB),
+ autorun: false,
+ });
+
+ const dbLogger = this.logger.createSubLogger('db');
+
+ this.dbQueueWorker
+ .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
+ //#endregion
- this.queueService.deliverQueue
- .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
+ //#region deliver
+ this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
+ ...baseQueueOptions(this.config, QUEUE.DELIVER),
+ autorun: false,
+ concurrency: this.config.deliverJobConcurrency ?? 128,
+ limiter: {
+ max: this.config.deliverJobPerSec ?? 128,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
+
+ const deliverLogger = this.logger.createSubLogger('deliver');
+
+ this.deliverQueueWorker
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
- .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
- .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
- .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
+ .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
+ .on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
+ //#endregion
+
+ //#region inbox
+ this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
+ ...baseQueueOptions(this.config, QUEUE.INBOX),
+ autorun: false,
+ concurrency: this.config.inboxJobConcurrency ?? 16,
+ limiter: {
+ max: this.config.inboxJobPerSec ?? 16,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
- this.queueService.inboxQueue
- .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
+ const inboxLogger = this.logger.createSubLogger('inbox');
+
+ this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
- .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
- .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
- .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
+ .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
+ //#endregion
- this.queueService.dbQueue
- .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
- .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
- .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) }))
- .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`));
-
- this.queueService.relationshipQueue
- .on('waiting', (jobId) => relationshipLogger.debug(`waiting id=${jobId}`))
- .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
- .on('error', (job: any, err: Error) => relationshipLogger.error(`error ${err}`, { job, e: renderError(err) }))
- .on('stalled', (job) => relationshipLogger.warn(`stalled id=${job.id}`));
+ //#region webhook deliver
+ this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), {
+ ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
+ autorun: false,
+ concurrency: 64,
+ limiter: {
+ max: 64,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
- this.queueService.objectStorageQueue
- .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`))
- .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
- .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
- .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
+ const webhookLogger = this.logger.createSubLogger('webhook');
- this.queueService.webhookDeliverQueue
- .on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
+ this.webhookDeliverQueueWorker
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
- .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
- .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
- .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
+ .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
+ .on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
+ //#endregion
- this.queueService.systemQueue.add('tickCharts', {
+ //#region relationship
+ this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
+ switch (job.name) {
+ case 'follow': return this.relationshipProcessorService.processFollow(job);
+ case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
+ case 'block': return this.relationshipProcessorService.processBlock(job);
+ case 'unblock': return this.relationshipProcessorService.processUnblock(job);
+ default: throw new Error(`unrecognized job type ${job.name} for relationship`);
+ }
}, {
- repeat: { cron: '55 * * * *' },
- removeOnComplete: true,
+ ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
+ autorun: false,
+ concurrency: this.config.relashionshipJobConcurrency ?? 16,
+ limiter: {
+ max: this.config.relashionshipJobPerSec ?? 64,
+ duration: 1000,
+ },
});
- this.queueService.systemQueue.add('resyncCharts', {
- }, {
- repeat: { cron: '0 0 * * *' },
- removeOnComplete: true,
- });
+ const relationshipLogger = this.logger.createSubLogger('relationship');
+
+ this.relationshipQueueWorker
+ .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
+ //#endregion
- this.queueService.systemQueue.add('cleanCharts', {
+ //#region object storage
+ this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
+ switch (job.name) {
+ case 'deleteFile': return this.deleteFileProcessorService.process(job);
+ case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
+ default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
+ }
}, {
- repeat: { cron: '0 0 * * *' },
- removeOnComplete: true,
+ ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
+ autorun: false,
+ concurrency: 16,
});
- this.queueService.systemQueue.add('aggregateRetention', {
- }, {
- repeat: { cron: '0 0 * * *' },
- removeOnComplete: true,
- });
+ const objectStorageLogger = this.logger.createSubLogger('objectStorage');
- this.queueService.systemQueue.add('clean', {
- }, {
- repeat: { cron: '0 0 * * *' },
- removeOnComplete: true,
- });
+ this.objectStorageQueueWorker
+ .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
+ //#endregion
- this.queueService.systemQueue.add('checkExpiredMutings', {
- }, {
- repeat: { cron: '*/5 * * * *' },
- removeOnComplete: true,
+ //#region ended poll notification
+ this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), {
+ ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
+ autorun: false,
});
+ //#endregion
+ }
- this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job));
- this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job));
- this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done));
- this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job));
-
- this.queueService.dbQueue.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done));
- this.queueService.dbQueue.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done));
- this.queueService.dbQueue.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done));
- this.queueService.dbQueue.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done));
- this.queueService.dbQueue.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done));
- this.queueService.dbQueue.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done));
- this.queueService.dbQueue.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
- this.queueService.dbQueue.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
- this.queueService.dbQueue.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done));
- this.queueService.dbQueue.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
- this.queueService.dbQueue.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
- this.queueService.dbQueue.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
- this.queueService.dbQueue.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done));
- this.queueService.dbQueue.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
- this.queueService.dbQueue.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
- this.queueService.dbQueue.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
- this.queueService.dbQueue.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done));
- this.queueService.dbQueue.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));
+ @bindThis
+ public async start(): Promise<void> {
+ await Promise.all([
+ this.systemQueueWorker.run(),
+ this.dbQueueWorker.run(),
+ this.deliverQueueWorker.run(),
+ this.inboxQueueWorker.run(),
+ this.webhookDeliverQueueWorker.run(),
+ this.relationshipQueueWorker.run(),
+ this.objectStorageQueueWorker.run(),
+ this.endedPollNotificationQueueWorker.run(),
+ ]);
+ }
- this.queueService.objectStorageQueue.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job));
- this.queueService.objectStorageQueue.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done));
-
- {
- const maxJobs = this.config.relashionshipJobConcurrency ?? 16;
- this.queueService.relationshipQueue.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
- this.queueService.relationshipQueue.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
- this.queueService.relationshipQueue.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
- this.queueService.relationshipQueue.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job));
- }
+ @bindThis
+ public async stop(): Promise<void> {
+ await Promise.all([
+ this.systemQueueWorker.close(),
+ this.dbQueueWorker.close(),
+ this.deliverQueueWorker.close(),
+ this.inboxQueueWorker.close(),
+ this.webhookDeliverQueueWorker.close(),
+ this.relationshipQueueWorker.close(),
+ this.objectStorageQueueWorker.close(),
+ this.endedPollNotificationQueueWorker.close(),
+ ]);
+ }
- this.queueService.systemQueue.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done));
- this.queueService.systemQueue.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done));
- this.queueService.systemQueue.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done));
- this.queueService.systemQueue.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done));
- this.queueService.systemQueue.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done));
- this.queueService.systemQueue.process('clean', (job, done) => this.cleanProcessorService.process(job, done));
+ @bindThis
+ public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
+ await this.stop();
}
}
diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts
new file mode 100644
index 0000000000..d240fe70e0
--- /dev/null
+++ b/packages/backend/src/queue/const.ts
@@ -0,0 +1,26 @@
+import { Config } from '@/config.js';
+import type * as Bull from 'bullmq';
+
+export const QUEUE = {
+ DELIVER: 'deliver',
+ INBOX: 'inbox',
+ SYSTEM: 'system',
+ ENDED_POLL_NOTIFICATION: 'endedPollNotification',
+ DB: 'db',
+ RELATIONSHIP: 'relationship',
+ OBJECT_STORAGE: 'objectStorage',
+ WEBHOOK_DELIVER: 'webhookDeliver',
+};
+
+export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
+ return {
+ connection: {
+ port: config.redisForJobQueue.port,
+ host: config.redisForJobQueue.host,
+ family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
+ password: config.redisForJobQueue.pass,
+ db: config.redisForJobQueue.db ?? 0,
+ },
+ prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`,
+ };
+}
diff --git a/packages/backend/src/queue/get-job-info.ts b/packages/backend/src/queue/get-job-info.ts
deleted file mode 100644
index d33e349c36..0000000000
--- a/packages/backend/src/queue/get-job-info.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import Bull from 'bull';
-
-export function getJobInfo(job: Bull.Job, increment = false) {
- const age = Date.now() - job.timestamp;
-
- const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
- : age > 10000 ? `${Math.floor(age / 1000)}s`
- : `${age}ms`;
-
- // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
- const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
- const maxAttempts = job.opts ? job.opts.attempts : 0;
-
- return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
-}
diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
index e2720b4fe0..600ce0828f 100644
--- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
+++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
@@ -9,7 +9,7 @@ import { deepClone } from '@/misc/clone.js';
import { IdService } from '@/core/IdService.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
@Injectable()
export class AggregateRetentionProcessorService {
@@ -32,7 +32,7 @@ export class AggregateRetentionProcessorService {
}
@bindThis
- public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
+ public async process(): Promise<void> {
this.logger.info('Aggregating retention...');
const now = new Date();
@@ -62,7 +62,6 @@ export class AggregateRetentionProcessorService {
} catch (err) {
if (isDuplicateKeyValueError(err)) {
this.logger.succ('Skip because it has already been processed by another worker.');
- done();
return;
}
throw err;
@@ -88,6 +87,5 @@ export class AggregateRetentionProcessorService {
}
this.logger.succ('Retention aggregated.');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
index 2476d71a5e..c4ee212bab 100644
--- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
+++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
@@ -7,7 +7,7 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
@Injectable()
export class CheckExpiredMutingsProcessorService {
@@ -27,7 +27,7 @@ export class CheckExpiredMutingsProcessorService {
}
@bindThis
- public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
+ public async process(): Promise<void> {
this.logger.info('Checking expired mutings...');
const expired = await this.mutingsRepository.createQueryBuilder('muting')
@@ -41,6 +41,5 @@ export class CheckExpiredMutingsProcessorService {
}
this.logger.succ('All expired mutings checked.');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts
index b458167042..22d7c1b4fb 100644
--- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts
@@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
@Injectable()
export class CleanChartsProcessorService {
@@ -45,7 +45,7 @@ export class CleanChartsProcessorService {
}
@bindThis
- public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
+ public async process(): Promise<void> {
this.logger.info('Clean charts...');
await Promise.all([
@@ -64,6 +64,5 @@ export class CleanChartsProcessorService {
]);
this.logger.succ('All charts successfully cleaned.');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts
index 1936e8df23..cefa6da5e9 100644
--- a/packages/backend/src/queue/processors/CleanProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanProcessorService.ts
@@ -7,7 +7,7 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
@Injectable()
export class CleanProcessorService {
@@ -36,7 +36,7 @@ export class CleanProcessorService {
}
@bindThis
- public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
+ public async process(): Promise<void> {
this.logger.info('Cleaning...');
this.userIpsRepository.delete({
@@ -72,6 +72,5 @@ export class CleanProcessorService {
}
this.logger.succ('Cleaned.');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
index 5a33c27188..c54bf59ae4 100644
--- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
@@ -5,9 +5,9 @@ import type { DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
-import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type * as Bull from 'bullmq';
@Injectable()
export class CleanRemoteFilesProcessorService {
@@ -27,7 +27,7 @@ export class CleanRemoteFilesProcessorService {
}
@bindThis
- public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<Record<string, unknown>>): Promise<void> {
this.logger.info('Deleting cached remote files...');
let deletedCount = 0;
@@ -47,7 +47,7 @@ export class CleanRemoteFilesProcessorService {
});
if (files.length === 0) {
- job.progress(100);
+ job.updateProgress(100);
break;
}
@@ -62,10 +62,9 @@ export class CleanRemoteFilesProcessorService {
isLink: false,
});
- job.progress(deletedCount / total);
+ job.updateProgress(deletedCount / total);
}
this.logger.succ('All cached remote files has been deleted.');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
index e36a78de6a..39dd801af0 100644
--- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
@@ -8,10 +8,10 @@ import { DriveService } from '@/core/DriveService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Note } from '@/models/entities/Note.js';
import { EmailService } from '@/core/EmailService.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbUserDeleteJobData } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class DeleteAccountProcessorService {
diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts
index 604497cf54..6772c5dc76 100644
--- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts
@@ -5,10 +5,10 @@ import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class DeleteDriveFilesProcessorService {
@@ -31,12 +31,11 @@ export class DeleteDriveFilesProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
this.logger.info(`Deleting drive files of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -56,7 +55,7 @@ export class DeleteDriveFilesProcessorService {
});
if (files.length === 0) {
- job.progress(100);
+ job.updateProgress(100);
break;
}
@@ -71,10 +70,9 @@ export class DeleteDriveFilesProcessorService {
userId: user.id,
});
- job.progress(deletedCount / total);
+ job.updateProgress(deletedCount / total);
}
this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
- done();
}
}
diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts
index 2fb2f56f8d..edf87bd921 100644
--- a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts
@@ -3,10 +3,10 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { ObjectStorageFileJobData } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class DeleteFileProcessorService {
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index f293bd4d7e..406e9df850 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
+import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@@ -16,7 +17,6 @@ import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
import type { DeliverJobData } from '../types.js';
@Injectable()
@@ -121,15 +121,13 @@ export class DeliverProcessorService {
isSuspended: true,
});
});
- return `${host} is gone`;
+ throw new Bull.UnrecoverableError(`${host} is gone`);
}
- // HTTPステータスコード4xxはクライアントエラーであり、それはつまり
- // 何回再送しても成功することはないということなのでエラーにはしないでおく
- return `${res.statusCode} ${res.statusMessage}`;
+ throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
}
// 5xx etc.
- throw `${res.statusCode} ${res.statusMessage}`;
+ throw new Error(`${res.statusCode} ${res.statusMessage}`);
} else {
// DNS error, socket error, timeout ...
throw res;
diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
index 501ed4090a..21501592f2 100644
--- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
+++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
@@ -6,7 +6,7 @@ import type Logger from '@/logger.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { EndedPollNotificationJobData } from '../types.js';
@Injectable()
@@ -30,10 +30,9 @@ export class EndedPollNotificationProcessorService {
}
@bindThis
- public async process(job: Bull.Job<EndedPollNotificationJobData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<EndedPollNotificationJobData>): Promise<void> {
const note = await this.notesRepository.findOneBy({ id: job.data.noteId });
if (note == null || !note.hasPoll) {
- done();
return;
}
@@ -51,7 +50,5 @@ export class EndedPollNotificationProcessorService {
noteId: note.id,
});
}
-
- done();
}
}
diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
index 894903e79b..ac52325c8d 100644
--- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
@@ -12,7 +12,7 @@ import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { DBExportAntennasData } from '../types.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
@Injectable()
export class ExportAntennasProcessorService {
@@ -39,10 +39,9 @@ export class ExportAntennasProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DBExportAntennasData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DBExportAntennasData>): Promise<void> {
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
const [path, cleanup] = await createTemp();
@@ -96,7 +95,6 @@ export class ExportAntennasProcessorService {
this.logger.succ('Exported to: ' + driveFile.id);
} finally {
cleanup();
- done();
}
}
}
diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
index c7b54070d6..eb758e162d 100644
--- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
@@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class ExportBlockingProcessorService {
@@ -36,12 +36,11 @@ export class ExportBlockingProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
this.logger.info(`Exporting blocking of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -69,7 +68,7 @@ export class ExportBlockingProcessorService {
});
if (blockings.length === 0) {
- job.progress(100);
+ job.updateProgress(100);
break;
}
@@ -99,7 +98,7 @@ export class ExportBlockingProcessorService {
blockerId: user.id,
});
- job.progress(exportedCount / total);
+ job.updateProgress(exportedCount / total);
}
stream.end();
@@ -112,7 +111,5 @@ export class ExportBlockingProcessorService {
} finally {
cleanup();
}
-
- done();
}
}
diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
index b50f373ef8..3203d9f3e5 100644
--- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
@@ -13,7 +13,7 @@ import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
@Injectable()
export class ExportCustomEmojisProcessorService {
@@ -37,12 +37,11 @@ export class ExportCustomEmojisProcessorService {
}
@bindThis
- public async process(job: Bull.Job, done: () => void): Promise<void> {
+ public async process(job: Bull.Job): Promise<void> {
this.logger.info('Exporting custom emojis ...');
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -117,24 +116,26 @@ export class ExportCustomEmojisProcessorService {
metaStream.end();
// Create archive
- const [archivePath, archiveCleanup] = await createTemp();
- const archiveStream = fs.createWriteStream(archivePath);
- const archive = archiver('zip', {
- zlib: { level: 0 },
- });
- archiveStream.on('close', async () => {
- this.logger.succ(`Exported to: ${archivePath}`);
+ await new Promise<void>(async (resolve) => {
+ const [archivePath, archiveCleanup] = await createTemp();
+ const archiveStream = fs.createWriteStream(archivePath);
+ const archive = archiver('zip', {
+ zlib: { level: 0 },
+ });
+ archiveStream.on('close', async () => {
+ this.logger.succ(`Exported to: ${archivePath}`);
- const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip';
- const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
+ const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip';
+ const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
- this.logger.succ(`Exported to: ${driveFile.id}`);
- cleanup();
- archiveCleanup();
- done();
+ this.logger.succ(`Exported to: ${driveFile.id}`);
+ cleanup();
+ archiveCleanup();
+ resolve();
+ });
+ archive.pipe(archiveStream);
+ archive.directory(path, false);
+ archive.finalize();
});
- archive.pipe(archiveStream);
- archive.directory(path, false);
- archive.finalize();
}
}
diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
index f2f2383a88..76c38a6b86 100644
--- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
@@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js';
import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@Injectable()
@@ -42,12 +42,11 @@ export class ExportFavoritesProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
this.logger.info(`Exporting favorites of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -91,7 +90,7 @@ export class ExportFavoritesProcessorService {
}) as (NoteFavorite & { note: Note & { user: User } })[];
if (favorites.length === 0) {
- job.progress(100);
+ job.updateProgress(100);
break;
}
@@ -112,7 +111,7 @@ export class ExportFavoritesProcessorService {
userId: user.id,
});
- job.progress(exportedFavoritesCount / total);
+ job.updateProgress(exportedFavoritesCount / total);
}
await write(']');
@@ -127,8 +126,6 @@ export class ExportFavoritesProcessorService {
} finally {
cleanup();
}
-
- done();
}
}
diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
index fa9c1ac1ea..8726cb1402 100644
--- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
@@ -10,10 +10,10 @@ import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import type { Following } from '@/models/entities/Following.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbExportFollowingData } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class ExportFollowingProcessorService {
@@ -40,12 +40,11 @@ export class ExportFollowingProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbExportFollowingData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbExportFollowingData>): Promise<void> {
this.logger.info(`Exporting following of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -116,7 +115,5 @@ export class ExportFollowingProcessorService {
} finally {
cleanup();
}
-
- done();
}
}
diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
index b14bf5f5b1..0f11a9e843 100644
--- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
@@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class ExportMutingProcessorService {
@@ -39,12 +39,11 @@ export class ExportMutingProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
this.logger.info(`Exporting muting of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -73,7 +72,7 @@ export class ExportMutingProcessorService {
});
if (mutes.length === 0) {
- job.progress(100);
+ job.updateProgress(100);
break;
}
@@ -103,7 +102,7 @@ export class ExportMutingProcessorService {
muterId: user.id,
});
- job.progress(exportedCount / total);
+ job.updateProgress(exportedCount / total);
}
stream.end();
@@ -116,7 +115,5 @@ export class ExportMutingProcessorService {
} finally {
cleanup();
}
-
- done();
}
}
diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
index e4f12ad101..24fb331883 100644
--- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
@@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js';
import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@Injectable()
@@ -39,12 +39,11 @@ export class ExportNotesProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
this.logger.info(`Exporting notes of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -87,7 +86,7 @@ export class ExportNotesProcessorService {
}) as Note[];
if (notes.length === 0) {
- job.progress(100);
+ job.updateProgress(100);
break;
}
@@ -108,7 +107,7 @@ export class ExportNotesProcessorService {
userId: user.id,
});
- job.progress(exportedNotesCount / total);
+ job.updateProgress(exportedNotesCount / total);
}
await write(']');
@@ -123,8 +122,6 @@ export class ExportNotesProcessorService {
} finally {
cleanup();
}
-
- done();
}
}
diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
index 54bde44044..ec63358053 100644
--- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
@@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class ExportUserListsProcessorService {
@@ -39,12 +39,11 @@ export class ExportUserListsProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
this.logger.info(`Exporting user lists of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -92,7 +91,5 @@ export class ExportUserListsProcessorService {
} finally {
cleanup();
}
-
- done();
}
}
diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
index d06131b8c8..575cad69d5 100644
--- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import { DBAntennaImportJobData } from '../types.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
const validate = new Ajv().compile({
type: 'object',
@@ -59,7 +59,7 @@ export class ImportAntennasProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DBAntennaImportJobData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DBAntennaImportJobData>): Promise<void> {
const now = new Date();
try {
for (const antenna of job.data.antenna) {
@@ -89,8 +89,6 @@ export class ImportAntennasProcessorService {
}
} catch (err: any) {
this.logger.error(err);
- } finally {
- done();
}
}
}
diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts
index 3f075b02d2..2f1a9e5b03 100644
--- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts
@@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DownloadService } from '@/core/DownloadService.js';
import { UtilityService } from '@/core/UtilityService.js';
-import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
-import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
import { bindThis } from '@/decorators.js';
import { QueueService } from '@/core/QueueService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type * as Bull from 'bullmq';
+import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
@Injectable()
export class ImportBlockingProcessorService {
@@ -34,12 +34,11 @@ export class ImportBlockingProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
this.logger.info(`Importing blocking of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -47,7 +46,6 @@ export class ImportBlockingProcessorService {
id: job.data.fileId,
});
if (file == null) {
- done();
return;
}
@@ -56,7 +54,6 @@ export class ImportBlockingProcessorService {
this.queueService.createImportBlockingToDbJob({ id: user.id }, targets);
this.logger.succ('Import jobs created');
- done();
}
@bindThis
@@ -85,7 +82,7 @@ export class ImportBlockingProcessorService {
}
if (target == null) {
- throw `Unable to resolve user: @${username}@${host}`;
+ throw new Error(`Unable to resolve user: @${username}@${host}`);
}
// skip myself
diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
index cf78d8330c..d862567871 100644
--- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
@@ -12,7 +12,7 @@ import { DriveService } from '@/core/DriveService.js';
import { DownloadService } from '@/core/DownloadService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbUserImportJobData } from '../types.js';
// TODO: 名前衝突時の動作を選べるようにする
@@ -45,14 +45,13 @@ export class ImportCustomEmojisProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
this.logger.info('Importing custom emojis ...');
const file = await this.driveFilesRepository.findOneBy({
id: job.data.fileId,
});
if (file == null) {
- done();
return;
}
@@ -107,13 +106,15 @@ export class ImportCustomEmojisProcessorService {
aliases: emojiInfo.aliases,
driveFile,
license: emojiInfo.license,
+ isSensitive: emojiInfo.isSensitive,
+ localOnly: emojiInfo.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: [],
});
}
cleanup();
this.logger.succ('Imported');
- done();
});
unzipStream.pipe(extractor);
this.logger.succ(`Unzipping to ${outputPath}`);
diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts
index aa5cf12c50..15bee9672e 100644
--- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts
@@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DownloadService } from '@/core/DownloadService.js';
import { UtilityService } from '@/core/UtilityService.js';
-import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
-import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
import { bindThis } from '@/decorators.js';
import { QueueService } from '@/core/QueueService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type * as Bull from 'bullmq';
+import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
@Injectable()
export class ImportFollowingProcessorService {
@@ -34,12 +34,11 @@ export class ImportFollowingProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
this.logger.info(`Importing following of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -47,7 +46,6 @@ export class ImportFollowingProcessorService {
id: job.data.fileId,
});
if (file == null) {
- done();
return;
}
@@ -56,7 +54,6 @@ export class ImportFollowingProcessorService {
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets);
this.logger.succ('Import jobs created');
- done();
}
@bindThis
@@ -85,7 +82,7 @@ export class ImportFollowingProcessorService {
}
if (target == null) {
- throw `Unable to resolve user: @${username}@${host}`;
+ throw new Error(`Unable to resolve user: @${username}@${host}`);
}
// skip myself
diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts
index 379994ee79..723935cd31 100644
--- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts
@@ -9,10 +9,10 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DownloadService } from '@/core/DownloadService.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbUserImportJobData } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class ImportMutingProcessorService {
@@ -38,12 +38,11 @@ export class ImportMutingProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
this.logger.info(`Importing muting of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -51,7 +50,6 @@ export class ImportMutingProcessorService {
id: job.data.fileId,
});
if (file == null) {
- done();
return;
}
@@ -83,7 +81,7 @@ export class ImportMutingProcessorService {
}
if (target == null) {
- throw `cannot resolve user: @${username}@${host}`;
+ throw new Error(`cannot resolve user: @${username}@${host}`);
}
// skip myself
@@ -98,6 +96,5 @@ export class ImportMutingProcessorService {
}
this.logger.succ('Imported');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
index c423863410..824ee8157a 100644
--- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
@@ -12,7 +12,7 @@ import { IdService } from '@/core/IdService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import type { DbUserImportJobData } from '../types.js';
@Injectable()
@@ -46,12 +46,11 @@ export class ImportUserListsProcessorService {
}
@bindThis
- public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
+ public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
this.logger.info(`Importing user lists of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
- done();
return;
}
@@ -59,7 +58,6 @@ export class ImportUserListsProcessorService {
id: job.data.fileId,
});
if (file == null) {
- done();
return;
}
@@ -109,6 +107,5 @@ export class ImportUserListsProcessorService {
}
this.logger.succ('Imported');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index ab8b1e9e22..ce1d7aaa1b 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -1,8 +1,8 @@
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import httpSignature from '@peertube/http-signature';
+import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
-import type { InstancesRepository, DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { MetaService } from '@/core/MetaService.js';
@@ -23,10 +23,8 @@ import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
import type { InboxJobData } from '../types.js';
-// ユーザーのinboxにアクティビティが届いた時の処理
@Injectable()
export class InboxProcessorService {
private logger: Logger;
@@ -35,12 +33,6 @@ export class InboxProcessorService {
@Inject(DI.config)
private config: Config,
- @Inject(DI.instancesRepository)
- private instancesRepository: InstancesRepository,
-
- @Inject(DI.driveFilesRepository)
- private driveFilesRepository: DriveFilesRepository,
-
private utilityService: UtilityService,
private metaService: MetaService,
private apInboxService: ApInboxService,
@@ -93,24 +85,24 @@ export class InboxProcessorService {
try {
authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor));
} catch (err) {
- // 対象が4xxならスキップ
+ // 対象が4xxならスキップ
if (err instanceof StatusError) {
if (err.isClientError) {
- return `skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`;
+ throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
}
- throw `Error in actor ${activity.actor} - ${err.statusCode ?? err}`;
+ throw new Error(`Error in actor ${activity.actor} - ${err.statusCode ?? err}`);
}
}
}
// それでもわからなければ終了
if (authUser == null) {
- return 'skip: failed to resolve user';
+ throw new Bull.UnrecoverableError('skip: failed to resolve user');
}
// publicKey がなくても終了
if (authUser.key == null) {
- return 'skip: failed to resolve user publicKey';
+ throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey');
}
// HTTP-Signatureの検証
@@ -118,10 +110,10 @@ export class InboxProcessorService {
// また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
- // 一致しなくても、でもLD-Signatureがありそうならそっちも見る
+ // 一致しなくても、でもLD-Signatureがありそうならそっちも見る
if (activity.signature) {
if (activity.signature.type !== 'RsaSignature2017') {
- return `skip: unsupported LD-signature type ${activity.signature.type}`;
+ throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`);
}
// activity.signature.creator: https://example.oom/users/user#main-key
@@ -134,32 +126,32 @@ export class InboxProcessorService {
// keyIdからLD-Signatureのユーザーを取得
authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator);
if (authUser == null) {
- return 'skip: LD-Signatureのユーザーが取得できませんでした';
+ throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
}
if (authUser.key == null) {
- return 'skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした';
+ throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
}
// LD-Signature検証
const ldSignature = this.ldSignatureService.use();
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
if (!verified) {
- return 'skip: LD-Signatureの検証に失敗しました';
+ throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
}
// もう一度actorチェック
if (authUser.user.uri !== activity.actor) {
- return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
+ throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
}
// ブロックしてたら中断
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
- return `Blocked request: ${ldHost}`;
+ throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
}
} else {
- return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
+ throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
}
}
@@ -168,7 +160,7 @@ export class InboxProcessorService {
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
const activityIdHost = this.utilityService.extractDbHost(activity.id);
if (signerHost !== activityIdHost) {
- return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
+ throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
}
}
diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts
index ff454df455..722260d948 100644
--- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts
+++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts
index e5840f3da8..eab8e1e68d 100644
--- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts
@@ -15,7 +15,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
@Injectable()
export class ResyncChartsProcessorService {
@@ -43,7 +43,7 @@ export class ResyncChartsProcessorService {
}
@bindThis
- public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
+ public async process(): Promise<void> {
this.logger.info('Resync charts...');
// TODO: ユーザーごとのチャートも更新する
@@ -55,6 +55,5 @@ export class ResyncChartsProcessorService {
]);
this.logger.succ('All charts successfully resynced.');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts
index 7ff84c15a5..f1696bf567 100644
--- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts
+++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts
@@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
+import type * as Bull from 'bullmq';
@Injectable()
export class TickChartsProcessorService {
@@ -45,7 +45,7 @@ export class TickChartsProcessorService {
}
@bindThis
- public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
+ public async process(): Promise<void> {
this.logger.info('Tick charts...');
await Promise.all([
@@ -64,6 +64,5 @@ export class TickChartsProcessorService {
]);
this.logger.succ('All charts successfully ticked.');
- done();
}
}
diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
index 84a5c21c49..8b40c16749 100644
--- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
+import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { WebhooksRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@@ -7,7 +8,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type Bull from 'bull';
import type { WebhookDeliverJobData } from '../types.js';
@Injectable()
@@ -66,11 +66,11 @@ export class WebhookDeliverProcessorService {
if (res instanceof StatusError) {
// 4xx
if (res.isClientError) {
- return `${res.statusCode} ${res.statusMessage}`;
+ throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
}
// 5xx etc.
- throw `${res.statusCode} ${res.statusMessage}`;
+ throw new Error(`${res.statusCode} ${res.statusMessage}`);
} else {
// DNS error, socket error, timeout ...
throw res;
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index e675d9cf1b..455acd1e47 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -585,7 +585,7 @@ export class ActivityPubServerService {
name: request.params.emoji,
});
- if (emoji == null) {
+ if (emoji == null || emoji.localOnly) {
reply.code(404);
return;
}
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 9257fee13e..c3d45e4ad6 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -194,7 +194,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.clientServerService.createServer);
- this.streamingApiServerService.attachStreamingApi(fastify.server);
+ this.streamingApiServerService.attach(fastify.server);
fastify.server.on('error', err => {
switch ((err as any).code) {
@@ -222,7 +222,14 @@ export class ServerService implements OnApplicationShutdown {
await fastify.ready();
}
- async onApplicationShutdown(signal: string): Promise<void> {
+ @bindThis
+ public async dispose(): Promise<void> {
+ await this.streamingApiServerService.detach();
await this.#fastify.close();
}
+
+ @bindThis
+ async onApplicationShutdown(signal: string): Promise<void> {
+ await this.dispose();
+ }
}
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index e3483c82c6..dad1a4132a 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -359,7 +359,12 @@ export class ApiCallService implements OnApplicationShutdown {
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined) {
+ public dispose(): void {
clearInterval(this.userIpHistoriesClearIntervalId);
}
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts
index 6548c475b2..e23591d876 100644
--- a/packages/backend/src/server/api/AuthenticateService.ts
+++ b/packages/backend/src/server/api/AuthenticateService.ts
@@ -36,7 +36,7 @@ export class AuthenticateService {
}
@bindThis
- public async authenticate(token: string | null | undefined): Promise<[LocalUser | null | undefined, AccessToken | null | undefined]> {
+ public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> {
if (token == null) {
return [null, null];
}
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index ee1aae5b6c..1e32e9988d 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -321,6 +321,9 @@ import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
+import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
+import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
+import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
import * as ep___users_notes from './endpoints/users/notes.js';
import * as ep___users_pages from './endpoints/users/pages.js';
import * as ep___users_reactions from './endpoints/users/reactions.js';
@@ -659,6 +662,9 @@ const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass:
const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default };
const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default };
const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
+const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
+const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
+const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default };
const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default };
@@ -1001,6 +1007,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_lists_push,
$users_lists_show,
$users_lists_update,
+ $users_lists_favorite,
+ $users_lists_unfavorite,
+ $users_lists_create_from_public,
$users_notes,
$users_pages,
$users_reactions,
@@ -1335,6 +1344,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_lists_push,
$users_lists_show,
$users_lists_update,
+ $users_lists_favorite,
+ $users_lists_unfavorite,
+ $users_lists_create_from_public,
$users_notes,
$users_pages,
$users_reactions,
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index 258e8de034..893dfe956e 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -1,23 +1,27 @@
import { EventEmitter } from 'events';
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import * as websocket from 'websocket';
+import * as WebSocket from 'ws';
import { DI } from '@/di-symbols.js';
-import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js';
+import type { UsersRepository, AccessToken } from '@/models/index.js';
import type { Config } from '@/config.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
-import { AuthenticateService } from './AuthenticateService.js';
+import { LocalUser } from '@/models/entities/User';
+import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/index.js';
import { ChannelsService } from './stream/ChannelsService.js';
-import type { ParsedUrlQuery } from 'querystring';
import type * as http from 'node:http';
@Injectable()
export class StreamingApiServerService {
+ #wss: WebSocket.WebSocketServer;
+ #connections = new Map<WebSocket.WebSocket, number>();
+ #cleanConnectionsIntervalId: NodeJS.Timeout | null = null;
+
constructor(
@Inject(DI.config)
private config: Config,
@@ -28,24 +32,6 @@ export class StreamingApiServerService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- @Inject(DI.followingsRepository)
- private followingsRepository: FollowingsRepository,
-
- @Inject(DI.mutingsRepository)
- private mutingsRepository: MutingsRepository,
-
- @Inject(DI.renoteMutingsRepository)
- private renoteMutingsRepository: RenoteMutingsRepository,
-
- @Inject(DI.blockingsRepository)
- private blockingsRepository: BlockingsRepository,
-
- @Inject(DI.channelFollowingsRepository)
- private channelFollowingsRepository: ChannelFollowingsRepository,
-
- @Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
-
private cacheService: CacheService,
private noteReadService: NoteReadService,
private authenticateService: AuthenticateService,
@@ -55,25 +41,65 @@ export class StreamingApiServerService {
}
@bindThis
- public attachStreamingApi(server: http.Server) {
- // Init websocket server
- const ws = new websocket.server({
- httpServer: server,
+ public attach(server: http.Server): void {
+ this.#wss = new WebSocket.WebSocketServer({
+ noServer: true,
});
- ws.on('request', async (request) => {
- const q = request.resourceURL.query as ParsedUrlQuery;
+ server.on('upgrade', async (request, socket, head) => {
+ if (request.url == null) {
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
+ socket.destroy();
+ return;
+ }
+
+ const q = new URL(request.url, `http://${request.headers.host}`).searchParams;
+
+ let user: LocalUser | null = null;
+ let app: AccessToken | null = null;
- // TODO: トークンが間違ってるなどしてauthenticateに失敗したら
- // コネクション切断するなりエラーメッセージ返すなりする
- // (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので)
- const [user, miapp] = await this.authenticateService.authenticate(q.i as string);
+ try {
+ [user, app] = await this.authenticateService.authenticate(q.get('i'));
+ } catch (e) {
+ if (e instanceof AuthenticationError) {
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
+ } else {
+ socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
+ }
+ socket.destroy();
+ return;
+ }
if (user?.isSuspended) {
- request.reject(400);
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
+ socket.destroy();
return;
}
+ const stream = new MainStreamConnection(
+ this.channelsService,
+ this.noteReadService,
+ this.notificationService,
+ this.cacheService,
+ user, app,
+ );
+
+ await stream.init();
+
+ this.#wss.handleUpgrade(request, socket, head, (ws) => {
+ this.#wss.emit('connection', ws, request, {
+ stream, user, app,
+ });
+ });
+ });
+
+ this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: {
+ stream: MainStreamConnection,
+ user: LocalUser | null;
+ app: AccessToken | null
+ }) => {
+ const { stream, user, app } = ctx;
+
const ev = new EventEmitter();
async function onRedisMessage(_: string, data: string): Promise<void> {
@@ -83,21 +109,11 @@ export class StreamingApiServerService {
this.redisForSub.on('message', onRedisMessage);
- const main = new MainStreamConnection(
- this.channelsService,
- this.noteReadService,
- this.notificationService,
- this.cacheService,
- ev, user, miapp,
- );
+ await stream.listen(ev, connection);
- await main.init();
+ this.#connections.set(connection, Date.now());
- const connection = request.accept();
-
- main.init2(connection);
-
- const intervalId = user ? setInterval(() => {
+ const userUpdateIntervalId = user ? setInterval(() => {
this.usersRepository.update(user.id, {
lastActiveDate: new Date(),
});
@@ -110,16 +126,38 @@ export class StreamingApiServerService {
connection.once('close', () => {
ev.removeAllListeners();
- main.dispose();
+ stream.dispose();
this.redisForSub.off('message', onRedisMessage);
- if (intervalId) clearInterval(intervalId);
+ if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
});
connection.on('message', async (data) => {
- if (data.type === 'utf8' && data.utf8Data === 'ping') {
+ this.#connections.set(connection, Date.now());
+ if (data.toString() === 'ping') {
connection.send('pong');
}
});
});
+
+ this.#cleanConnectionsIntervalId = setInterval(() => {
+ const now = Date.now();
+ for (const [connection, lastActive] of this.#connections.entries()) {
+ if (now - lastActive > 1000 * 60 * 5) {
+ connection.terminate();
+ this.#connections.delete(connection);
+ }
+ }
+ }, 1000 * 60 * 5);
+ }
+
+ @bindThis
+ public detach(): Promise<void> {
+ if (this.#cleanConnectionsIntervalId) {
+ clearInterval(this.#cleanConnectionsIntervalId);
+ this.#cleanConnectionsIntervalId = null;
+ }
+ return new Promise((resolve) => {
+ this.#wss.close(() => resolve());
+ });
}
}
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 09bd7cbff4..7e678a6404 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -320,6 +320,9 @@ import * as ep___users_lists_list from './endpoints/users/lists/list.js';
import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
+import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
+import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
+import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
import * as ep___users_notes from './endpoints/users/notes.js';
import * as ep___users_pages from './endpoints/users/pages.js';
@@ -656,7 +659,10 @@ const eps = [
['users/lists/pull', ep___users_lists_pull],
['users/lists/push', ep___users_lists_push],
['users/lists/show', ep___users_lists_show],
+ ['users/lists/favorite', ep___users_lists_favorite],
+ ['users/lists/unfavorite', ep___users_lists_unfavorite],
['users/lists/update', ep___users_lists_update],
+ ['users/lists/create-from-public', ep___users_lists_create_from_public],
['users/notes', ep___users_notes],
['users/pages', ep___users_pages],
['users/reactions', ep___users_reactions],
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
index 2393c2441c..12db1f78fb 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
@@ -25,7 +25,7 @@ export const paramDef = {
id: { type: 'string', format: 'misskey:id' },
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
- imageUrl: { type: 'string', nullable: true, minLength: 1 },
+ imageUrl: { type: 'string', nullable: true, minLength: 0 },
},
required: ['id', 'title', 'text', 'imageUrl'],
} as const;
@@ -46,7 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
updatedAt: new Date(),
title: ps.title,
text: ps.text,
- imageUrl: ps.imageUrl,
+ /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
+ imageUrl: ps.imageUrl || null,
});
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index 2fb3e489e7..509224e9c3 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -25,9 +25,24 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
+ name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
fileId: { type: 'string', format: 'misskey:id' },
+ category: {
+ type: 'string',
+ nullable: true,
+ description: 'Use `null` to reset the category.',
+ },
+ aliases: { type: 'array', items: {
+ type: 'string',
+ } },
+ license: { type: 'string', nullable: true },
+ isSensitive: { type: 'boolean' },
+ localOnly: { type: 'boolean' },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
+ type: 'string',
+ } },
},
- required: ['fileId'],
+ required: ['name', 'fileId'],
} as const;
// TODO: ロジックをサービスに切り出す
@@ -45,18 +60,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
- const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
-
const emoji = await this.customEmojiService.add({
driveFile,
- name,
- category: null,
- aliases: [],
+ name: ps.name,
+ category: ps.category ?? null,
+ aliases: ps.aliases ?? [],
host: null,
- license: null,
+ license: ps.license ?? null,
+ isSensitive: ps.isSensitive ?? false,
+ localOnly: ps.localOnly ?? false,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
});
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index f63348b60b..fb22bdc477 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -1,6 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import type { DriveFilesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -15,6 +17,11 @@ export const meta = {
code: 'NO_SUCH_EMOJI',
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
},
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d',
+ },
sameNameEmojiExists: {
message: 'Emoji that have same name already exists.',
code: 'SAME_NAME_EMOJI_EXISTS',
@@ -28,6 +35,7 @@ export const paramDef = {
properties: {
id: { type: 'string', format: 'misskey:id' },
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
+ fileId: { type: 'string', format: 'misskey:id' },
category: {
type: 'string',
nullable: true,
@@ -37,6 +45,11 @@ export const paramDef = {
type: 'string',
} },
license: { type: 'string', nullable: true },
+ isSensitive: { type: 'boolean' },
+ localOnly: { type: 'boolean' },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
+ type: 'string',
+ } },
},
required: ['id', 'name', 'aliases'],
} as const;
@@ -45,14 +58,28 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
+
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
+ let driveFile;
+
+ if (ps.fileId) {
+ driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
+ if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
+ }
+
await this.customEmojiService.update(ps.id, {
+ driveFile,
name: ps.name,
category: ps.category ?? null,
aliases: ps.aliases,
license: ps.license ?? null,
+ isSensitive: ps.isSensitive,
+ localOnly: ps.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
});
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
index f12738bd3a..f2d4aa8996 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
try {
- if (new URL(ps.inbox).protocol !== 'https:') throw 'https only';
+ if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
} catch {
throw new ApiError(meta.errors.invalidUrl);
}
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index dca0f443b7..e756a9b510 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -113,6 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
this.antennasRepository.update(antenna.id, {
+ isActive: true,
lastUsedAt: new Date(),
});
diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts
index cb2e661bfb..05842460cf 100644
--- a/packages/backend/src/server/api/endpoints/auth/accept.ts
+++ b/packages/backend/src/server/api/endpoints/auth/accept.ts
@@ -55,7 +55,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchSession);
}
- // Generate access token
const accessToken = secureRndstr(32, true);
// Fetch exist access token
@@ -65,7 +64,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
if (exist == null) {
- // Lookup app
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
// Generate Hash
@@ -75,7 +73,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const now = new Date();
- // Insert access token doc
await this.accessTokensRepository.insert({
id: this.idService.genId(),
createdAt: now,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index ad33398da6..e8985a9cd8 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -1,6 +1,6 @@
import { promisify } from 'node:util';
import bcrypt from 'bcryptjs';
-import * as cbor from 'cbor';
+import cbor from 'cbor';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
index 3361e5a4d3..48fb03a8af 100644
--- a/packages/backend/src/server/api/endpoints/i/apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -26,7 +26,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const query = this.accessTokensRepository.createQueryBuilder('token')
- .where('token.userId = :userId', { userId: me.id });
+ .where('token.userId = :userId', { userId: me.id })
+ .leftJoinAndSelect('token.app', 'app');
switch (ps.sort) {
case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break;
@@ -40,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return await Promise.all(tokens.map(token => ({
id: token.id,
- name: token.name,
+ name: token.name ?? token.app?.name,
createdAt: token.createdAt,
lastUsedAt: token.lastUsedAt,
permission: token.permission,
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index e141be764a..f5662f4a0e 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -91,18 +91,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
- const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
+ const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const notificationsRes = await this.redisClient.xrevrange(
`notificationTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
- '-',
+ ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
'COUNT', limit);
if (notificationsRes.length === 0) {
return [];
}
- let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
+ let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[];
if (includeTypes && includeTypes.length > 0) {
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 74be00a8b8..8f5e6177c2 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -141,13 +141,12 @@ export const paramDef = {
preventAiLearning: { type: 'boolean' },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
- showTimelineReplies: { type: 'boolean' },
injectFeaturedNote: { type: 'boolean' },
receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' },
autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
- pinnedPageId: { type: 'string', format: 'misskey:id' },
+ pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: { type: 'array' },
mutedInstances: { type: 'array', items: {
type: 'string',
@@ -239,7 +238,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
- if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 584ea07c3b..53d724a9dd 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -1,5 +1,6 @@
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
+import * as JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/index.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -292,8 +293,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
- defaultLightTheme: instance.defaultLightTheme,
- defaultDarkTheme: instance.defaultDarkTheme,
+ // クライアントの手間を減らすためあらかじめJSONに変換しておく
+ defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
+ defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 3f7f2cdece..96be5ed844 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -99,7 +99,7 @@ export const paramDef = {
} },
cw: { type: 'string', nullable: true, maxLength: 100 },
localOnly: { type: 'boolean', default: false },
- reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote'], default: null },
+ reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false },
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 c11c1eac40..88c1ca7f58 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -34,11 +34,8 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
- },
+ withFiles: { type: 'boolean', default: false },
+ withReplies: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@@ -78,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
- this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateRepliesQuery(query, ps.withReplies, me);
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index 89abd91c7e..7a3581e6e4 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -46,11 +46,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
- },
+ withFiles: { type: 'boolean', default: false },
+ withReplies: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -98,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.setParameters(followingQuery.getParameters());
this.queryService.generateChannelQuery(query, me);
- this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
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 afdafc7c55..2ee549232c 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -36,11 +36,8 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
- },
+ withFiles: { type: 'boolean', default: false },
+ withReplies: { type: 'boolean', default: false },
fileType: { type: 'array', items: {
type: 'string',
} },
@@ -86,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateChannelQuery(query, me);
- this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateMutedNoteQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index 2956bf1cbd..742df0ca95 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
@@ -82,14 +82,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
if (ps.tag) {
- if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection';
+ if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection');
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
} else {
query.andWhere(new Brackets(qb => {
for (const tags of ps.query!) {
qb.orWhere(new Brackets(qb => {
for (const tag of tags) {
- if (!safeForSql(normalizeForSearch(tag))) throw 'Injection';
+ if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection');
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
}
}));
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index c6ee1e5c2b..e1f286439b 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -35,11 +35,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
- },
+ withFiles: { type: 'boolean', default: false },
+ withReplies: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -84,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
this.queryService.generateChannelQuery(query, me);
- this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts
index 4ced6d3ff1..1d4825f812 100644
--- a/packages/backend/src/server/api/endpoints/reset-db.ts
+++ b/packages/backend/src/server/api/endpoints/reset-db.ts
@@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private redisClient: Redis.Redis,
) {
super(meta, paramDef, async (ps, me) => {
- if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
+ if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test');
await redisClient.flushdb();
await resetDb(this.db);
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index 6202c740f1..42e36cb04a 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -93,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
+ .andWhere('(note.visibility = \'public\')')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
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
new file mode 100644
index 0000000000..8591e4ab96
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
@@ -0,0 +1,148 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import type { UserList } from '@/models/entities/UserList.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { RoleService } from '@/core/RoleService.js';
+import { UserListService } from '@/core/UserListService.js';
+
+export const meta = {
+ requireCredential: true,
+ prohibitMoved: true,
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserList',
+ },
+
+ errors: {
+ tooManyUserLists: {
+ message: 'You cannot create user list any more.',
+ code: 'TOO_MANY_USERLISTS',
+ id: 'e9c105b2-c595-47de-97fb-7f7c2c33e92f',
+ },
+ noSuchList: {
+ message: 'No such list.',
+ code: 'NO_SUCH_LIST',
+ id: '9292f798-6175-4f7d-93f4-b6742279667d',
+ },
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '13c457db-a8cb-4d88-b70a-211ceeeabb5f',
+ },
+
+ alreadyAdded: {
+ message: 'That user has already been added to that list.',
+ code: 'ALREADY_ADDED',
+ id: 'c3ad6fdb-692b-47ee-a455-7bd12c7af615',
+ },
+
+ youHaveBeenBlocked: {
+ message: 'You cannot push this user because you have been blocked by this user.',
+ code: 'YOU_HAVE_BEEN_BLOCKED',
+ id: 'a2497f2a-2389-439c-8626-5298540530f4',
+ },
+
+ tooManyUsers: {
+ message: 'You can not push users any more.',
+ code: 'TOO_MANY_USERS',
+ id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ name: { type: 'string', minLength: 1, maxLength: 100 },
+ listId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['name', 'listId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.userListsRepository)
+ private userListsRepository: UserListsRepository,
+
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
+
+ @Inject(DI.blockingsRepository)
+ private blockingsRepository: BlockingsRepository,
+
+ private userListService: UserListService,
+ private userListEntityService: UserListEntityService,
+ private idService: IdService,
+ private getterService: GetterService,
+ private roleService: RoleService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const list = await this.userListsRepository.findOneBy({
+ id: ps.listId,
+ isPublic: true,
+ });
+ if (list === null) throw new ApiError(meta.errors.noSuchList);
+ const currentCount = await this.userListsRepository.countBy({
+ userId: me.id,
+ });
+ if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
+ throw new ApiError(meta.errors.tooManyUserLists);
+ }
+
+ const userList = await this.userListsRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ userId: me.id,
+ name: ps.name,
+ } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
+
+ const users = (await this.userListJoiningsRepository.findBy({
+ userListId: ps.listId,
+ })).map(x => x.userId);
+
+ for (const user of users) {
+ const currentUser = await this.getterService.getUser(user).catch(err => {
+ if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+ throw err;
+ });
+
+ if (currentUser.id !== me.id) {
+ const block = await this.blockingsRepository.findOneBy({
+ blockerId: currentUser.id,
+ blockeeId: me.id,
+ });
+ if (block) {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ }
+ }
+
+ const exist = await this.userListJoiningsRepository.findOneBy({
+ userListId: userList.id,
+ userId: currentUser.id,
+ });
+
+ if (exist) {
+ throw new ApiError(meta.errors.alreadyAdded);
+ }
+
+ try {
+ await this.userListService.push(currentUser, userList, me);
+ } catch (err) {
+ if (err instanceof UserListService.TooManyUsersError) {
+ throw new ApiError(meta.errors.tooManyUsers);
+ }
+ throw err;
+ }
+ }
+ return await this.userListEntityService.pack(userList);
+ });
+ }
+}
+
diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts
new file mode 100644
index 0000000000..263852fde1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts
@@ -0,0 +1,70 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import { ApiError } from '@/server/api/error.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ requireCredential: true,
+ errors: {
+ noSuchList: {
+ message: 'No such user list.',
+ code: 'NO_SUCH_USER_LIST',
+ id: '7dbaf3cf-7b42-4b8f-b431-b3919e580dbe',
+ },
+
+ alreadyFavorited: {
+ message: 'The list has already been favorited.',
+ code: 'ALREADY_FAVORITED',
+ id: '6425bba0-985b-461e-af1b-518070e72081',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ listId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['listId'],
+} as const;
+
+@Injectable() // eslint-disable-next-line import/no-default-export
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor (
+ @Inject(DI.userListsRepository)
+ private userListsRepository: UserListsRepository,
+
+ @Inject(DI.userListFavoritesRepository)
+ private userListFavoritesRepository: UserListFavoritesRepository,
+ private idService: IdService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const userList = await this.userListsRepository.findOneBy({
+ id: ps.listId,
+ isPublic: true,
+ });
+
+ if (userList === null) {
+ throw new ApiError(meta.errors.noSuchList);
+ }
+
+ const exist = await this.userListFavoritesRepository.findOneBy({
+ userId: me.id,
+ userListId: ps.listId,
+ });
+
+ if (exist !== null) {
+ throw new ApiError(meta.errors.alreadyFavorited);
+ }
+
+ await this.userListFavoritesRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ userId: me.id,
+ userListId: ps.listId,
+ });
+ });
+ }
+}
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 2104c4377d..eab29944b2 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/list.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts
@@ -1,13 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { UserListsRepository } from '@/models/index.js';
+import type { UserListsRepository, UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
+import { ApiError } from '@/server/api/error.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['lists', 'account'],
- requireCredential: true,
+ requireCredential: false,
kind: 'read:account',
@@ -22,26 +23,58 @@ export const meta = {
ref: 'UserList',
},
},
+ errors: {
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: 'a8af4a82-0980-4cc4-a6af-8b0ffd54465e',
+ },
+ remoteUser: {
+ message: 'Not allowed to load the remote user\'s list',
+ code: 'REMOTE_USER_NOT_ALLOWED',
+ id: '53858f1b-3315-4a01-81b7-db9b48d4b79a',
+ },
+ invalidParam: {
+ message: 'Invalid param.',
+ code: 'INVALID_PARAM',
+ id: 'ab36de0e-29e9-48cb-9732-d82f1281620d',
+ },
+ },
} as const;
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
required: [],
} as const;
-// eslint-disable-next-line import/no-default-export
-@Injectable()
+@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
private userListEntityService: UserListEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const userLists = await this.userListsRepository.findBy({
+ if (typeof ps.userId !== 'undefined') {
+ const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ if (user === null) throw new ApiError(meta.errors.noSuchUser);
+ if (user.host !== null) throw new ApiError(meta.errors.remoteUser);
+ } else if (me === null) {
+ throw new ApiError(meta.errors.invalidParam);
+ }
+
+ const userLists = await this.userListsRepository.findBy(typeof ps.userId === 'undefined' && me !== null ? {
userId: me.id,
+ } : {
+ userId: ps.userId,
+ isPublic: true,
});
return await Promise.all(userLists.map(x => this.userListEntityService.pack(x)));
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 77f9cba808..8077841c8c 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { UserListsRepository } from '@/models/index.js';
+import type { UserListsRepository, UserListFavoritesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -8,7 +8,7 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['lists', 'account'],
- requireCredential: true,
+ requireCredential: false,
kind: 'read:account',
@@ -33,31 +33,54 @@ export const paramDef = {
type: 'object',
properties: {
listId: { type: 'string', format: 'misskey:id' },
+ forPublic: { type: 'boolean', default: false },
},
required: ['listId'],
} as const;
-// eslint-disable-next-line import/no-default-export
-@Injectable()
+@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
+ @Inject(DI.userListFavoritesRepository)
+ private userListFavoritesRepository: UserListFavoritesRepository,
+
private userListEntityService: UserListEntityService,
) {
super(meta, paramDef, async (ps, me) => {
+ const additionalProperties: Partial<{ likedCount: number, isLiked: boolean }> = {};
// Fetch the list
- const userList = await this.userListsRepository.findOneBy({
+ const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? {
id: ps.listId,
userId: me.id,
+ } : {
+ id: ps.listId,
+ isPublic: true,
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchList);
}
- return await this.userListEntityService.pack(userList);
+ if (ps.forPublic && userList.isPublic) {
+ additionalProperties.likedCount = await this.userListFavoritesRepository.countBy({
+ userListId: ps.listId,
+ });
+ if (me !== null) {
+ additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({
+ userId: me.id,
+ userListId: ps.listId,
+ }) !== null);
+ } else {
+ additionalProperties.isLiked = false;
+ }
+ }
+ return {
+ ...await this.userListEntityService.pack(userList),
+ ...additionalProperties,
+ };
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts
new file mode 100644
index 0000000000..be8e317816
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts
@@ -0,0 +1,63 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js';
+import { ApiError } from '@/server/api/error.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ requireCredential: true,
+ errors: {
+ noSuchList: {
+ message: 'No such user list.',
+ code: 'NO_SUCH_USER_LIST',
+ id: 'baedb33e-76b8-4b0c-86a8-9375c0a7b94b',
+ },
+
+ notFavorited: {
+ message: 'You have not favorited the list.',
+ code: 'ALREADY_FAVORITED',
+ id: '835c4b27-463d-4cfa-969b-a9058678d465',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ listId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['listId'],
+} as const;
+
+@Injectable() // eslint-disable-next-line import/no-default-export
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor (
+ @Inject(DI.userListsRepository)
+ private userListsRepository: UserListsRepository,
+
+ @Inject(DI.userListFavoritesRepository)
+ private userListFavoritesRepository: UserListFavoritesRepository,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const userList = await this.userListsRepository.findOneBy({
+ id: ps.listId,
+ isPublic: true,
+ });
+
+ if (userList === null) {
+ throw new ApiError(meta.errors.noSuchList);
+ }
+
+ const exist = await this.userListFavoritesRepository.findOneBy({
+ userListId: ps.listId,
+ userId: me.id,
+ });
+
+ if (exist === null) {
+ throw new ApiError(meta.errors.notFavorited);
+ }
+
+ await this.userListFavoritesRepository.delete({ id: exist.id });
+ });
+ }
+}
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 6453d7d980..b0a95a2f28 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/update.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts
@@ -34,8 +34,9 @@ export const paramDef = {
properties: {
listId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
+ isPublic: { type: 'boolean' },
},
- required: ['listId', 'name'],
+ required: ['listId'],
} as const;
// eslint-disable-next-line import/no-default-export
@@ -48,7 +49,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userListEntityService: UserListEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- // Fetch the list
const userList = await this.userListsRepository.findOneBy({
id: ps.listId,
userId: me.id,
@@ -60,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.userListsRepository.update(userList.id, {
name: ps.name,
+ isPublic: ps.isPublic,
});
return await this.userListEntityService.pack(userList.id);
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 5454836fe1..d3339072c1 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -13,6 +13,7 @@ class GlobalTimelineChannel extends Channel {
public readonly chName = 'globalTimeline';
public static shouldShare = true;
public static requireCredential = false;
+ private withReplies: boolean;
constructor(
private metaService: MetaService,
@@ -31,6 +32,8 @@ class GlobalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return;
+ this.withReplies = params.withReplies as boolean;
+
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@@ -54,7 +57,7 @@ class GlobalTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && !this.user!.showTimelineReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index ee874ad81e..1755aa94cf 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -11,6 +11,7 @@ class HomeTimelineChannel extends Channel {
public readonly chName = 'homeTimeline';
public static shouldShare = true;
public static requireCredential = true;
+ private withReplies: boolean;
constructor(
private noteEntityService: NoteEntityService,
@@ -24,6 +25,8 @@ class HomeTimelineChannel extends Channel {
@bindThis
public async init(params: any) {
+ this.withReplies = params.withReplies as boolean;
+
this.subscriber.on('notesStream', this.onNote);
}
@@ -63,7 +66,7 @@ class HomeTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && !this.user!.showTimelineReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 4f7b4e78b6..5a33e13cf5 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -13,6 +13,7 @@ class HybridTimelineChannel extends Channel {
public readonly chName = 'hybridTimeline';
public static shouldShare = true;
public static requireCredential = true;
+ private withReplies: boolean;
constructor(
private metaService: MetaService,
@@ -31,6 +32,8 @@ class HybridTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
+ this.withReplies = params.withReplies as boolean;
+
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@@ -75,7 +78,7 @@ class HybridTimelineChannel extends Channel {
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
// 関係ない返信は除外
- if (note.reply && !this.user!.showTimelineReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 09b0005ac1..9ca4db8ced 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -12,6 +12,7 @@ class LocalTimelineChannel extends Channel {
public readonly chName = 'localTimeline';
public static shouldShare = true;
public static requireCredential = false;
+ private withReplies: boolean;
constructor(
private metaService: MetaService,
@@ -30,6 +31,8 @@ class LocalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
+ this.withReplies = params.withReplies as boolean;
+
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@@ -54,7 +57,7 @@ class LocalTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && this.user && !this.user.showTimelineReplies) {
+ if (note.reply && this.user && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index 9d106c8b2f..ab9c1aa0b5 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -5,15 +5,17 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import Channel from '../channel.js';
import { StreamMessages } from '../types.js';
+import { RoleService } from '@/core/RoleService.js';
class RoleTimelineChannel extends Channel {
public readonly chName = 'roleTimeline';
public static shouldShare = false;
public static requireCredential = false;
private roleId: string;
-
+
constructor(
private noteEntityService: NoteEntityService,
+ private roleservice: RoleService,
id: string,
connection: Channel['connection'],
@@ -34,6 +36,11 @@ class RoleTimelineChannel extends Channel {
if (data.type === 'note') {
const note = data.body;
+ if (!(await this.roleservice.isExplorable({ id: this.roleId }))) {
+ return;
+ }
+ if (note.visibility !== 'public') return;
+
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
@@ -61,6 +68,7 @@ export class RoleTimelineChannelService {
constructor(
private noteEntityService: NoteEntityService,
+ private roleservice: RoleService,
) {
}
@@ -68,6 +76,7 @@ export class RoleTimelineChannelService {
public create(id: string, connection: Channel['connection']): RoleTimelineChannel {
return new RoleTimelineChannel(
this.noteEntityService,
+ this.roleservice,
id,
connection,
);
diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts
index a6f9145952..8b1c2c09c9 100644
--- a/packages/backend/src/server/api/stream/index.ts
+++ b/packages/backend/src/server/api/stream/index.ts
@@ -1,3 +1,4 @@
+import * as WebSocket from 'ws';
import type { User } from '@/models/entities/User.js';
import type { AccessToken } from '@/models/entities/AccessToken.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -7,7 +8,6 @@ import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserProfile } from '@/models/index.js';
import type { ChannelsService } from './ChannelsService.js';
-import type * as websocket from 'websocket';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
import type { StreamEventEmitter, StreamMessages } from './types.js';
@@ -18,7 +18,7 @@ import type { StreamEventEmitter, StreamMessages } from './types.js';
export default class Connection {
public user?: User;
public token?: AccessToken;
- private wsConnection: websocket.connection;
+ private wsConnection: WebSocket.WebSocket;
public subscriber: StreamEventEmitter;
private channels: Channel[] = [];
private subscribingNotes: any = {};
@@ -37,11 +37,9 @@ export default class Connection {
private notificationService: NotificationService,
private cacheService: CacheService,
- subscriber: EventEmitter,
user: User | null | undefined,
token: AccessToken | null | undefined,
) {
- this.subscriber = subscriber;
if (user) this.user = user;
if (token) this.token = token;
}
@@ -70,12 +68,16 @@ export default class Connection {
if (this.user != null) {
await this.fetch();
- this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);
+ if (!this.fetchIntervalId) {
+ this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);
+ }
}
}
@bindThis
- public async init2(wsConnection: websocket.connection) {
+ public async listen(subscriber: EventEmitter, wsConnection: WebSocket.WebSocket) {
+ this.subscriber = subscriber;
+
this.wsConnection = wsConnection;
this.wsConnection.on('message', this.onWsConnectionMessage);
@@ -88,14 +90,11 @@ export default class Connection {
* クライアントからメッセージ受信時
*/
@bindThis
- private async onWsConnectionMessage(data: websocket.Message) {
- if (data.type !== 'utf8') return;
- if (data.utf8Data == null) return;
-
+ private async onWsConnectionMessage(data: WebSocket.RawData) {
let obj: Record<string, any>;
try {
- obj = JSON.parse(data.utf8Data);
+ obj = JSON.parse(data.toString());
} catch (e) {
return;
}
@@ -246,7 +245,7 @@ export default class Connection {
const ch: Channel = channelService.create(id, this);
this.channels.push(ch);
- ch.init(params);
+ ch.init(params ?? {});
if (pong) {
this.sendMessageToWs('connected', {
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index fd7f54da54..38ae8ad2e5 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -116,9 +116,9 @@
}
}
}
- const colorSchema = localStorage.getItem('colorSchema');
- if (colorSchema) {
- document.documentElement.style.setProperty('color-schema', colorSchema);
+ const colorScheme = localStorage.getItem('colorScheme');
+ if (colorScheme) {
+ document.documentElement.style.setProperty('color-scheme', colorScheme);
}
//#endregion
@@ -160,37 +160,41 @@
<path d="M12 9v2m0 4v.01"></path>
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
</svg>
- <h1>An error has occurred!</h1>
- <button class="button-big" onclick="location.reload();">
- <span class="button-label-big">Refresh</span>
+ <h1>Failed to load<br>読み込みに失敗しました</h1>
+ <button class="button-big" onclick="location.reload(true);">
+ <span class="button-label-big">Reload / リロード</span>
</button>
- <p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
- <p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
- <p>Update your os and browser.</p>
- <p>Disable an adblocker.</p>
- <a href="/flush">
- <button class="button-small">
- <span class="button-label-small">Clear preferences and cache</span>
- </button>
- </a>
- <br>
- <a href="/cli">
- <button class="button-small">
- <span class="button-label-small">Start the simple client</span>
- </button>
- </a>
- <br>
- <a href="/bios">
- <button class="button-small">
- <span class="button-label-small">Start the repair tool</span>
- </button>
- </a>
+ <p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
+ <p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
+ <p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
+ <p>Disable an adblocker / アドブロッカーを無効にする</p>
+ <details style="color: #86b300;">
+ <summary>Other options / その他のオプション</summary>
+ <a href="/flush">
+ <button class="button-small">
+ <span class="button-label-small">Clear preferences and cache</span>
+ </button>
+ </a>
+ <br>
+ <a href="/cli">
+ <button class="button-small">
+ <span class="button-label-small">Start the simple client</span>
+ </button>
+ </a>
+ <br>
+ <a href="/bios">
+ <button class="button-small">
+ <span class="button-label-small">Start the repair tool</span>
+ </button>
+ </a>
+ </details>
<br>
<div id="errors"></div>
`;
errorsElement = document.getElementById('errors');
}
const detailsElement = document.createElement('details');
+ detailsElement.id = 'errorInfo';
detailsElement.innerHTML = `
<br>
<summary>
@@ -247,7 +251,7 @@
.button-label-big {
color: #222;
font-weight: bold;
- font-size: 20px;
+ font-size: 1.2em;
padding: 12px;
}
@@ -267,11 +271,6 @@
font-size: 16px;
}
- .dont-worry,
- #msg {
- font-size: 18px;
- }
-
.icon-warning {
color: #dec340;
height: 4rem;
@@ -279,14 +278,15 @@
}
h1 {
- font-size: 32px;
+ font-size: 1.5em;
+ margin: 1em;
}
code {
font-family: Fira, FiraCode, monospace;
}
- details {
+ #errorInfo {
background: #333;
margin-bottom: 2rem;
padding: 0.5rem 1rem;
@@ -296,16 +296,16 @@
margin: auto;
}
- summary {
+ #errorInfo summary {
cursor: pointer;
}
- summary > * {
+ #errorInfo summary > * {
display: inline;
}
@media screen and (max-width: 500px) {
- details {
+ #errorInfo {
width: 50%;
}
`)
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index cb5d05a403..69b3f68e05 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -25,7 +25,6 @@ html
meta(name='referrer' content='origin')
meta(name='theme-color' content= themeColor || '#86b300')
meta(name='theme-color-orig' content= themeColor || '#86b300')
- meta(property='twitter:card' content='summary')
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='icon' href= icon || '/favicon.ico')
@@ -36,7 +35,7 @@ html
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
//- https://github.com/misskey-dev/misskey/issues/9842
- link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.17.0')
+ link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists
@@ -59,6 +58,7 @@ html
meta(property='og:title' content= title || 'Misskey')
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
meta(property='og:image' content= img)
+ meta(property='twitter:card' content='summary')
style
include ../style.css
diff --git a/packages/backend/src/server/web/views/channel.pug b/packages/backend/src/server/web/views/channel.pug
index 486f0ecc47..c514025e0b 100644
--- a/packages/backend/src/server/web/views/channel.pug
+++ b/packages/backend/src/server/web/views/channel.pug
@@ -16,3 +16,4 @@ block og
meta(property='og:description' content= channel.description)
meta(property='og:url' content= url)
meta(property='og:image' content= channel.bannerUrl)
+ meta(property='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug
index 74dc62f1e7..5a0018803a 100644
--- a/packages/backend/src/server/web/views/clip.pug
+++ b/packages/backend/src/server/web/views/clip.pug
@@ -17,6 +17,7 @@ block og
meta(property='og:description' content= clip.description)
meta(property='og:url' content= url)
meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
block meta
if profile.noCrawle
diff --git a/packages/backend/src/server/web/views/flash.pug b/packages/backend/src/server/web/views/flash.pug
index 5594fcdfbf..1549aa7906 100644
--- a/packages/backend/src/server/web/views/flash.pug
+++ b/packages/backend/src/server/web/views/flash.pug
@@ -17,6 +17,7 @@ block og
meta(property='og:description' content= flash.summary)
meta(property='og:url' content= url)
meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
block meta
if profile.noCrawle
diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug
index 10f2d269bc..a458d7f8c7 100644
--- a/packages/backend/src/server/web/views/gallery-post.pug
+++ b/packages/backend/src/server/web/views/gallery-post.pug
@@ -17,6 +17,7 @@ block og
meta(property='og:description' content= post.description)
meta(property='og:url' content= url)
meta(property='og:image' content= post.files[0].thumbnailUrl)
+ meta(property='twitter:card' content='summary_large_image')
block meta
if user.host || profile.noCrawle
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
index badfcccd61..874c48c602 100644
--- a/packages/backend/src/server/web/views/note.pug
+++ b/packages/backend/src/server/web/views/note.pug
@@ -5,6 +5,8 @@ block vars
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
- const url = `${config.url}/notes/${note.id}`;
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
+ - const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.type.isSensitive)
+ - const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.type.isSensitive)
block title
= `${title} | ${instanceName}`
@@ -17,7 +19,19 @@ block og
meta(property='og:title' content= title)
meta(property='og:description' content= summary)
meta(property='og:url' content= url)
- meta(property='og:image' content= avatarUrl)
+ if video
+ 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)
+ // FIXME: add width and height
+ // FIXME: add embed player for Twitter
+ if image
+ meta(property='twitter:card' content='summary_large_image')
+ meta(property='og:image' content= image.url)
+ else
+ meta(property='twitter:card' content='summary')
+ meta(property='og:image' content= avatarUrl)
+
block meta
if user.host || isRenote || profile.noCrawle
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
index ddffc361c8..08bb08ffe7 100644
--- a/packages/backend/src/server/web/views/page.pug
+++ b/packages/backend/src/server/web/views/page.pug
@@ -17,6 +17,7 @@ block og
meta(property='og:description' content= page.summary)
meta(property='og:url' content= url)
meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
+ meta(property='twitter:card' content= page.eyeCatchingImage ? 'summary_large_image' : 'summary')
block meta
if profile.noCrawle
diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug
index f4c83aa89d..83d57349a6 100644
--- a/packages/backend/src/server/web/views/user.pug
+++ b/packages/backend/src/server/web/views/user.pug
@@ -16,6 +16,7 @@ block og
meta(property='og:description' content= profile.description)
meta(property='og:url' content= url)
meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
block meta
if user.host || profile.noCrawle
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 0addb430c9..5da997f28b 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -2,7 +2,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as crypto from 'node:crypto';
-import * as cbor from 'cbor';
+import cbor from 'cbor';
import * as OTPAuth from 'otpauth';
import { loadConfig } from '../../src/config.js';
import { signup, api, post, react, startServer, waitFire } from '../utils.js';
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
new file mode 100644
index 0000000000..dd3b09f85a
--- /dev/null
+++ b/packages/backend/test/e2e/antennas.ts
@@ -0,0 +1,653 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { inspect } from 'node:util';
+import { DEFAULT_POLICIES } from '@/core/RoleService.js';
+import type { Packed } from '@/misc/json-schema.js';
+import {
+ signup,
+ post,
+ userList,
+ page,
+ role,
+ startServer,
+ api,
+ successfulApiCall,
+ failedApiCall,
+ uploadFile,
+ testPaginationConsistency,
+} from '../utils.js';
+import type * as misskey from 'misskey-js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
+ return selector(a).localeCompare(selector(b));
+};
+
+describe('アンテナ', () => {
+ // エンティティとしてのアンテナを主眼においたテストを記述する
+ // (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからノートを取得するエンドポイントをテストする)
+
+ // BUG misskey-jsとjson-schemaが一致していない。
+ // - srcのenumにgroupが残っている
+ // - userGroupIdが残っている, isActiveがない
+ type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
+ type User = misskey.entities.MeDetailed & { token: string };
+ type Note = misskey.entities.Note;
+
+ // アンテナを作成できる最小のパラメタ
+ const defaultParam = {
+ caseSensitive: false,
+ excludeKeywords: [['']],
+ keywords: [['keyword']],
+ name: 'test',
+ notify: false,
+ src: 'all' as const,
+ userListId: null,
+ users: [''],
+ withFile: false,
+ withReplies: false,
+ };
+
+ let app: INestApplicationContext;
+
+ let root: User;
+ let alice: User;
+ let bob: User;
+ let carol: User;
+
+ let alicePost: Note;
+ let aliceList: misskey.entities.UserList;
+ let bobFile: misskey.entities.DriveFile;
+ let bobList: misskey.entities.UserList;
+
+ let userNotExplorable: User;
+ let userLocking: User;
+ let userSilenced: User;
+ let userSuspended: User;
+ let userDeletedBySelf: User;
+ let userDeletedByAdmin: User;
+ let userFollowingAlice: User;
+ let userFollowedByAlice: User;
+ let userBlockingAlice: User;
+ let userBlockedByAlice: User;
+ let userMutingAlice: User;
+ let userMutedByAlice: User;
+
+ beforeAll(async () => {
+ app = await startServer();
+ }, 1000 * 60 * 2);
+
+ beforeAll(async () => {
+ root = await signup({ username: 'root' });
+ alice = await signup({ username: 'alice' });
+ alicePost = await post(alice, { text: 'test' });
+ aliceList = await userList(alice, {});
+ bob = await signup({ username: 'bob' });
+ aliceList = await userList(alice, {});
+ bobFile = (await uploadFile(bob)).body;
+ bobList = await userList(bob);
+ carol = await signup({ username: 'carol' });
+ await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
+ await api('users/lists/push', { listId: aliceList.id, userId: carol.id }, alice);
+
+ userNotExplorable = await signup({ username: 'userNotExplorable' });
+ await post(userNotExplorable, { text: 'test' });
+ await api('i/update', { isExplorable: false }, userNotExplorable);
+ userLocking = await signup({ username: 'userLocking' });
+ await post(userLocking, { text: 'test' });
+ await api('i/update', { isLocked: true }, userLocking);
+ userSilenced = await signup({ username: 'userSilenced' });
+ await post(userSilenced, { text: 'test' });
+ const roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } });
+ await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
+ userSuspended = await signup({ username: 'userSuspended' });
+ await post(userSuspended, { text: 'test' });
+ await successfulApiCall({ endpoint: 'i/update', parameters: { description: '#user_testuserSuspended' }, user: userSuspended });
+ await api('admin/suspend-user', { userId: userSuspended.id }, root);
+ userDeletedBySelf = await signup({ username: 'userDeletedBySelf', password: 'userDeletedBySelf' });
+ await post(userDeletedBySelf, { text: 'test' });
+ await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf);
+ userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' });
+ await post(userDeletedByAdmin, { text: 'test' });
+ await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root);
+ userFollowedByAlice = await signup({ username: 'userFollowedByAlice' });
+ await post(userFollowedByAlice, { text: 'test' });
+ await api('following/create', { userId: userFollowedByAlice.id }, alice);
+ userFollowingAlice = await signup({ username: 'userFollowingAlice' });
+ await post(userFollowingAlice, { text: 'test' });
+ await api('following/create', { userId: alice.id }, userFollowingAlice);
+ userBlockingAlice = await signup({ username: 'userBlockingAlice' });
+ await post(userBlockingAlice, { text: 'test' });
+ await api('blocking/create', { userId: alice.id }, userBlockingAlice);
+ userBlockedByAlice = await signup({ username: 'userBlockedByAlice' });
+ await post(userBlockedByAlice, { text: 'test' });
+ await api('blocking/create', { userId: userBlockedByAlice.id }, alice);
+ userMutingAlice = await signup({ username: 'userMutingAlice' });
+ await post(userMutingAlice, { text: 'test' });
+ await api('mute/create', { userId: alice.id }, userMutingAlice);
+ userMutedByAlice = await signup({ username: 'userMutedByAlice' });
+ await post(userMutedByAlice, { text: 'test' });
+ await api('mute/create', { userId: userMutedByAlice.id }, alice);
+ }, 1000 * 60 * 10);
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ beforeEach(async () => {
+ // テスト間で影響し合わないように毎回全部消す。
+ for (const user of [alice, bob]) {
+ const list = await api('/antennas/list', {}, user);
+ for (const antenna of list.body) {
+ await api('/antennas/delete', { antennaId: antenna.id }, user);
+ }
+ }
+ });
+
+ //#region 作成(antennas/create)
+
+ test('が作成できること、キーが過不足なく入っていること。', async () => {
+ const response = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ });
+ assert.match(response.id, /[0-9a-z]{10}/);
+ const expected = {
+ id: response.id,
+ caseSensitive: false,
+ createdAt: new Date(response.createdAt).toISOString(),
+ excludeKeywords: [['']],
+ hasUnreadNote: false,
+ isActive: true,
+ keywords: [['keyword']],
+ name: 'test',
+ notify: false,
+ src: 'all',
+ userListId: null,
+ users: [''],
+ withFile: false,
+ withReplies: false,
+ } as Antenna;
+ assert.deepStrictEqual(response, expected);
+ });
+
+ test('が上限いっぱいまで作成できること', async () => {
+ // antennaLimit + 1まで作れるのがキモ
+ const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ })));
+
+ const expected = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice });
+ assert.deepStrictEqual(
+ response.sort(compareBy(s => s.id)),
+ expected.sort(compareBy(s => s.id)));
+
+ failedApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'TOO_MANY_ANTENNAS',
+ id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
+ });
+ });
+
+ test('を作成するとき他人のリストを指定したらエラーになる', async () => {
+ failedApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, src: 'list', userListId: bobList.id },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'NO_SUCH_USER_LIST',
+ id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f',
+ });
+ });
+
+ const antennaParamPattern = [
+ { parameters: (): object => ({ name: 'x'.repeat(100) }) },
+ { parameters: (): object => ({ name: 'x' }) },
+ { parameters: (): object => ({ src: 'home' }) },
+ { parameters: (): object => ({ src: 'all' }) },
+ { parameters: (): object => ({ src: 'users' }) },
+ { parameters: (): object => ({ src: 'list' }) },
+ { parameters: (): object => ({ userListId: null }) },
+ { parameters: (): object => ({ src: 'list', userListId: aliceList.id }) },
+ { parameters: (): object => ({ keywords: [['x']] }) },
+ { parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
+ { parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
+ { parameters: (): object => ({ users: [alice.username] }) },
+ { parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) },
+ { parameters: (): object => ({ caseSensitive: false }) },
+ { parameters: (): object => ({ caseSensitive: true }) },
+ { parameters: (): object => ({ withReplies: false }) },
+ { parameters: (): object => ({ withReplies: true }) },
+ { parameters: (): object => ({ withFile: false }) },
+ { parameters: (): object => ({ withFile: true }) },
+ { parameters: (): object => ({ notify: false }) },
+ { parameters: (): object => ({ notify: true }) },
+ ];
+ test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
+ const response = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, ...parameters() },
+ user: alice,
+ });
+ const expected = { ...response, ...parameters() };
+ assert.deepStrictEqual(response, expected);
+ });
+
+ //#endregion
+ //#region 更新(antennas/update)
+
+ test.each(antennaParamPattern)('を変更できること($#)', async ({ parameters }) => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/update',
+ parameters: { antennaId: antenna.id, ...defaultParam, ...parameters() },
+ user: alice,
+ });
+ const expected = { ...response, ...parameters() };
+ assert.deepStrictEqual(response, expected);
+ });
+ test.todo('は他人のものは変更できない');
+
+ test('を変更するとき他人のリストを指定したらエラーになる', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ failedApiCall({
+ endpoint: 'antennas/update',
+ parameters: { antennaId: antenna.id, ...defaultParam, src: 'list', userListId: bobList.id },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'NO_SUCH_USER_LIST',
+ id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
+ });
+ });
+
+ //#endregion
+ //#region 表示(antennas/show)
+
+ test('をID指定で表示できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/show',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ const expected = { ...antenna };
+ assert.deepStrictEqual(response, expected);
+ });
+ test.todo('は他人のものをID指定で表示できない');
+
+ //#endregion
+ //#region 一覧(antennas/list)
+
+ test('をリスト形式で取得できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: bob });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/list',
+ parameters: {},
+ user: alice,
+ });
+ const expected = [{ ...antenna }];
+ assert.deepStrictEqual(response, expected);
+ });
+
+ //#endregion
+ //#region 削除(antennas/delete)
+
+ test('を削除できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/delete',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ assert.deepStrictEqual(response, null);
+ const list = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice });
+ assert.deepStrictEqual(list, []);
+ });
+ test.todo('は他人のものを削除できない');
+
+ //#endregion
+
+ describe('のノート', () => {
+ //#region アンテナのノート取得(antennas/notes)
+
+ test('を取得できること。', async () => {
+ const keyword = 'キーワード';
+ await post(bob, { text: `test ${keyword} beforehand` });
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]] },
+ user: alice,
+ });
+ const note = await post(bob, { text: `test ${keyword}` });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ const expected = [note];
+ assert.deepStrictEqual(response, expected);
+ });
+
+ const keyword = 'キーワード';
+ test.each([
+ {
+ label: '全体から',
+ parameters: (): object => ({ src: 'all' }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ // BUG e4144a1 以降home指定は壊れている(allと同じ)
+ label: 'ホーム指定はallと同じ',
+ parameters: (): object => ({ src: 'home' }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ // https://github.com/misskey-dev/misskey/issues/9025
+ label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }) },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }) },
+ ],
+ },
+ {
+ label: 'ブロックしているユーザーのノートは含む',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'ブロックされているユーザーのノートは含まない',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) },
+ ],
+ },
+ {
+ label: 'ミュートしているユーザーのノートは含まない',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) },
+ ],
+ },
+ {
+ label: 'ミュートされているユーザーのノートは含む',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '「見つけやすくする」がOFFのユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '鍵付きユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'サイレンスのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '削除ユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'ユーザー指定で',
+ parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'リスト指定で',
+ parameters: (): object => ({ src: 'list', userListId: aliceList.id }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'CWにもマッチする',
+ parameters: (): object => ({ keywords: [[keyword]] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード1つ',
+ parameters: (): object => ({ keywords: [[keyword]] }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: 'test' }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: 'test' }) },
+ ],
+ },
+ {
+ label: 'キーワード3つ(AND)',
+ parameters: (): object => ({ keywords: [['A', 'B', 'C']] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'test A' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'test A B' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'test B C' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'test A B C' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test C B A A B C' }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード3つ(OR)',
+ parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'test' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test A B' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test B C' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test B C A' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test C B' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test C' }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード3つ(AND)',
+ parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A B` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C A` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C B` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C` }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード3つ(OR)',
+ parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A B` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C A` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C B` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C` }) },
+ ],
+ },
+ {
+ label: 'キーワード1つ(大文字小文字区別する)',
+ parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'keyword' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'KEYWORD' }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード1つ(大文字小文字区別しない)',
+ parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'KEYWORD' }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード1つ(大文字小文字区別する)',
+ parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} kEyWoRd` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} KEYWORD` }) },
+ ],
+ },
+ {
+ label: '除外ワード1つ(大文字小文字区別しない)',
+ parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} kEyWoRd` }) },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} KEYWORD` }) },
+ ],
+ },
+ {
+ label: '添付ファイルを問わない',
+ parameters: (): object => ({ withFile: false }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '添付ファイル付きのみ',
+ parameters: (): object => ({ withFile: true }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }) },
+ ],
+ },
+ {
+ label: 'リプライ以外',
+ parameters: (): object => ({ withReplies: false }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'リプライも含む',
+ parameters: (): object => ({ withReplies: true }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ ],
+ },
+ ])('が取得できること($label)', async ({ parameters, posts }) => {
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]], ...parameters() },
+ user: alice,
+ });
+
+ const notes = await posts.reduce(async (prev, current) => {
+ // includedに関わらずnote()は評価して投稿する。
+ const p = await prev;
+ const n = await current.note();
+ if (current.included) return p.concat(n);
+ return p;
+ }, Promise.resolve([] as Note[]));
+
+ // alice視点でNoteを取り直す
+ const expected = await Promise.all(notes.reverse().map(s => successfulApiCall({
+ endpoint: 'notes/show',
+ parameters: { noteId: s.id },
+ user: alice,
+ })));
+
+ const response = await successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ assert.deepStrictEqual(
+ response.map(({ userId, id, text }) => ({ userId, id, text })),
+ expected.map(({ userId, id, text }) => ({ userId, id, text })));
+ assert.deepStrictEqual(response, expected);
+ });
+
+ test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
+ test.each([
+ { label: 'ID指定', offsetBy: 'id' },
+
+ // BUG sinceDate, untilDateはsinceIdや他のエンドポイントとは異なり、その時刻に一致するレコードを含んでしまう。
+ // { label: '日付指定', offsetBy: 'createdAt' },
+ ] as const)('が取得でき、$labelのPaginationに一貫性があること', async ({ offsetBy }) => {
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]] },
+ user: alice,
+ });
+ const notes = await [...Array(30)].reduce(async (prev, current, index) => {
+ const p = await prev;
+ const n = await post(alice, { text: `${keyword} (${index})` });
+ return [n].concat(p);
+ }, Promise.resolve([] as Note[]));
+
+ // antennas/notesは降順のみで、昇順をサポートしない。
+ await testPaginationConsistency(notes, async (paginationParam) => {
+ return successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id, ...paginationParam },
+ user: alice,
+ }) as any as Note[];
+ }, offsetBy, 'desc');
+ });
+
+ // BUG 7日過ぎると作り直すしかない。 https://github.com/misskey-dev/misskey/issues/10476
+ test.todo('を取得したときActiveに戻る');
+
+ //#endregion
+ });
+});
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index a7f8210c8e..02684c93b8 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -43,7 +43,6 @@ describe('ユーザー', () => {
type MeDetailed = UserDetailedNotMe &
misskey.entities.MeDetailed & {
- showTimelineReplies: boolean,
achievements: object[],
loggedInDays: number,
policies: object,
@@ -160,7 +159,6 @@ describe('ユーザー', () => {
mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes,
emailNotificationTypes: user.emailNotificationTypes,
- showTimelineReplies: user.showTimelineReplies,
achievements: user.achievements,
loggedInDays: user.loggedInDays,
policies: user.policies,
@@ -406,7 +404,6 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.mutedInstances, []);
assert.deepStrictEqual(response.mutingNotificationTypes, []);
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
- assert.strictEqual(response.showTimelineReplies, false);
assert.deepStrictEqual(response.achievements, []);
assert.deepStrictEqual(response.loggedInDays, 0);
assert.deepStrictEqual(response.policies, DEFAULT_POLICIES);
@@ -470,8 +467,6 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) },
{ parameters: (): object => ({ isCat: false }) },
- { parameters: (): object => ({ showTimelineReplies: true }) },
- { parameters: (): object => ({ showTimelineReplies: false }) },
{ parameters: (): object => ({ injectFeaturedNote: true }) },
{ parameters: (): object => ({ injectFeaturedNote: false }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },
diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts
index 6b31e68616..a7bcd859ae 100644
--- a/packages/backend/test/misc/mock-resolver.ts
+++ b/packages/backend/test/misc/mock-resolver.ts
@@ -52,11 +52,7 @@ export class MockResolver extends Resolver {
const r = this._rs.get(value);
if (!r) {
- throw {
- name: 'StatusError',
- statusCode: 404,
- message: 'Not registed for mock',
- };
+ throw new Error('Not registed for mock');
}
const object = JSON.parse(r.content);
diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts
index 38db081ac0..aa68f4117d 100644
--- a/packages/backend/test/unit/ReactionService.ts
+++ b/packages/backend/test/unit/ReactionService.ts
@@ -15,78 +15,74 @@ describe('ReactionService', () => {
reactionService = app.get<ReactionService>(ReactionService);
});
- describe('toDbReaction', () => {
+ describe('normalize', () => {
test('絵文字リアクションはそのまま', async () => {
- assert.strictEqual(await reactionService.toDbReaction('👍'), '👍');
- assert.strictEqual(await reactionService.toDbReaction('🍅'), '🍅');
+ assert.strictEqual(await reactionService.normalize('👍'), '👍');
+ assert.strictEqual(await reactionService.normalize('🍅'), '🍅');
});
test('既存のリアクションは絵文字化する pudding', async () => {
- assert.strictEqual(await reactionService.toDbReaction('pudding'), '🍮');
+ assert.strictEqual(await reactionService.normalize('pudding'), '🍮');
});
test('既存のリアクションは絵文字化する like', async () => {
- assert.strictEqual(await reactionService.toDbReaction('like'), '👍');
+ assert.strictEqual(await reactionService.normalize('like'), '👍');
});
test('既存のリアクションは絵文字化する love', async () => {
- assert.strictEqual(await reactionService.toDbReaction('love'), '❤');
+ assert.strictEqual(await reactionService.normalize('love'), '❤');
});
test('既存のリアクションは絵文字化する laugh', async () => {
- assert.strictEqual(await reactionService.toDbReaction('laugh'), '😆');
+ assert.strictEqual(await reactionService.normalize('laugh'), '😆');
});
test('既存のリアクションは絵文字化する hmm', async () => {
- assert.strictEqual(await reactionService.toDbReaction('hmm'), '🤔');
+ assert.strictEqual(await reactionService.normalize('hmm'), '🤔');
});
test('既存のリアクションは絵文字化する surprise', async () => {
- assert.strictEqual(await reactionService.toDbReaction('surprise'), '😮');
+ assert.strictEqual(await reactionService.normalize('surprise'), '😮');
});
test('既存のリアクションは絵文字化する congrats', async () => {
- assert.strictEqual(await reactionService.toDbReaction('congrats'), '🎉');
+ assert.strictEqual(await reactionService.normalize('congrats'), '🎉');
});
test('既存のリアクションは絵文字化する angry', async () => {
- assert.strictEqual(await reactionService.toDbReaction('angry'), '💢');
+ assert.strictEqual(await reactionService.normalize('angry'), '💢');
});
test('既存のリアクションは絵文字化する confused', async () => {
- assert.strictEqual(await reactionService.toDbReaction('confused'), '😥');
+ assert.strictEqual(await reactionService.normalize('confused'), '😥');
});
test('既存のリアクションは絵文字化する rip', async () => {
- assert.strictEqual(await reactionService.toDbReaction('rip'), '😇');
+ assert.strictEqual(await reactionService.normalize('rip'), '😇');
});
test('既存のリアクションは絵文字化する star', async () => {
- assert.strictEqual(await reactionService.toDbReaction('star'), '⭐');
+ assert.strictEqual(await reactionService.normalize('star'), '⭐');
});
test('異体字セレクタ除去', async () => {
- assert.strictEqual(await reactionService.toDbReaction('㊗️'), '㊗');
+ assert.strictEqual(await reactionService.normalize('㊗️'), '㊗');
});
test('異体字セレクタ除去 必要なし', async () => {
- assert.strictEqual(await reactionService.toDbReaction('㊗'), '㊗');
- });
-
- test('fallback - undefined', async () => {
- assert.strictEqual(await reactionService.toDbReaction(undefined), '❤');
+ assert.strictEqual(await reactionService.normalize('㊗'), '㊗');
});
test('fallback - null', async () => {
- assert.strictEqual(await reactionService.toDbReaction(null), '❤');
+ assert.strictEqual(await reactionService.normalize(null), '❤');
});
test('fallback - empty', async () => {
- assert.strictEqual(await reactionService.toDbReaction(''), '❤');
+ assert.strictEqual(await reactionService.normalize(''), '❤');
});
test('fallback - unknown', async () => {
- assert.strictEqual(await reactionService.toDbReaction('unknown'), '❤');
+ assert.strictEqual(await reactionService.normalize('unknown'), '❤');
});
});
});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 809ed2c66c..22f7d81e4e 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -124,6 +124,13 @@ export const react = async (user: any, note: any, reaction: string): Promise<any
}, user);
};
+export const userList = async (user: any, userList: any = {}): Promise<any> => {
+ const res = await api('users/lists/create', {
+ name: 'test',
+ }, user);
+ return res.body;
+};
+
export const page = async (user: any, page: any = {}): Promise<any> => {
const res = await api('pages/create', {
alignCenter: false,
@@ -380,8 +387,98 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
};
};
+/**
+ * あるAPIエンドポイントのPaginationが複数の条件で一貫した挙動であることをテストします。
+ * (sinceId, untilId, sinceDate, untilDate, offset, limit)
+ * @param expected 期待値となるEntityの並び(例:Note[])昇順降順が一致している必要がある
+ * @param fetchEntities Entity[]を返却するテスト対象のAPIを呼び出す関数
+ * @param offsetBy 何をキーとしてPaginationするか。
+ * @param ordering 昇順・降順
+ */
+export async function testPaginationConsistency<Entity extends { id: string, createdAt?: string }>(
+ expected: Entity[],
+ fetchEntities: (paginationParam: {
+ limit?: number,
+ offset?: number,
+ sinceId?: string,
+ untilId?: string,
+ sinceDate?: number,
+ untilDate?: number,
+ }) => Promise<Entity[]>,
+ offsetBy: 'offset' | 'id' | 'createdAt' = 'id',
+ ordering: 'desc' | 'asc' = 'desc'): Promise<void> {
+ const rangeToParam = (p: { limit?: number, until?: Entity, since?: Entity }): object => {
+ if (offsetBy === 'id') {
+ return { limit: p.limit, sinceId: p.since?.id, untilId: p.until?.id };
+ } else {
+ const sinceDate = p.since?.createdAt !== undefined ? new Date(p.since.createdAt).getTime() : undefined;
+ const untilDate = p.until?.createdAt !== undefined ? new Date(p.until.createdAt).getTime() : undefined;
+ return { limit: p.limit, sinceDate, untilDate };
+ }
+ };
+
+ for (const limit of [1, 5, 10, 100, undefined]) {
+ // 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること
+ if (ordering === 'desc') {
+ const end = expected[expected.length - 1];
+ let last = await fetchEntities(rangeToParam({ limit, since: end }));
+ const actual: Entity[] = [];
+ while (last.length !== 0) {
+ actual.push(...last);
+ last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1], since: end }));
+ }
+ actual.push(end);
+ assert.deepStrictEqual(
+ actual.map(({ id, createdAt }) => id + ':' + createdAt),
+ expected.map(({ id, createdAt }) => id + ':' + createdAt));
+ }
+
+ // 2. sinceId/Date指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
+ if (ordering === 'asc') {
+ // 昇順にしたときの先頭(一番古いもの)をもってくる(expected[1]を基準に降順にして0番目)
+ let last = await fetchEntities({ limit: 1, untilId: expected[1].id });
+ const actual: Entity[] = [];
+ while (last.length !== 0) {
+ actual.push(...last);
+ last = await fetchEntities(rangeToParam({ limit, since: last[last.length - 1] }));
+ }
+ assert.deepStrictEqual(
+ actual.map(({ id, createdAt }) => id + ':' + createdAt),
+ expected.map(({ id, createdAt }) => id + ':' + createdAt));
+ }
+
+ // 3. untilId指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
+ if (ordering === 'desc') {
+ let last = await fetchEntities({ limit });
+ const actual: Entity[] = [];
+ while (last.length !== 0) {
+ actual.push(...last);
+ last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1] }));
+ }
+ assert.deepStrictEqual(
+ actual.map(({ id, createdAt }) => id + ':' + createdAt),
+ expected.map(({ id, createdAt }) => id + ':' + createdAt));
+ }
+
+ // 4. offset指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
+ if (offsetBy === 'offset') {
+ let last = await fetchEntities({ limit, offset: 0 });
+ let offset = limit ?? 10;
+ const actual: Entity[] = [];
+ while (last.length !== 0) {
+ actual.push(...last);
+ last = await fetchEntities({ limit, offset });
+ offset += limit ?? 10;
+ }
+ assert.deepStrictEqual(
+ actual.map(({ id, createdAt }) => id + ':' + createdAt),
+ expected.map(({ id, createdAt }) => id + ':' + createdAt));
+ }
+ }
+}
+
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
- if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
+ if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test');
const db = new DataSource({
type: 'postgres',