From 9cf023b6d1957b4bc4d4376f386c74f9e801828f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 02:58:05 +0900 Subject: [PATCH 01/10] i18n (MisskeyIO#732) --- locales/en-US.yml | 5 +++++ locales/ko-KR.yml | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index b0f728a62..7265b9ee8 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -442,6 +442,10 @@ moderation: "Moderation" moderationNote: "Moderation note" addModerationNote: "Add moderation note" moderationLogs: "Moderation logs" +userAccountMoveLogs: "Account migration logs" +userAccountMoveLogsTitle: "{from} migrated the account to {to}" +movedToId: "ID of the account migrated to" +moveFromId: "ID of the account migrated from" nUsersMentioned: "Mentioned by {n} users" securityKeyAndPasskey: "Security- and passkeys" securityKey: "Security key" @@ -1087,6 +1091,7 @@ audioFiles: "Audio" dataSaver: "Data Saver" accountMigration: "Account Migration" accountMoved: "This user has moved to a new account:" +accountMovedFrom: "This user has been migrated from the following account:" accountMovedShort: "This account has been migrated." operationForbidden: "Operation forbidden" forceShowAds: "Always show ads" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 4d465c6c9..4a6d50551 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -441,6 +441,10 @@ moderation: "조정" moderationNote: "조정 기록" addModerationNote: "조정 기록 추가하기" moderationLogs: "모더레이션 로그" +userAccountMoveLogs: "계정 이사 사용 로그" +userAccountMoveLogsTitle: "{from} 가 {to} 로 계정을 이사했습니다" +movedToId: "이사 후 계정의 ID" +moveFromId: "이사 전 계정의 ID" nUsersMentioned: "{n}명이 언급함" securityKeyAndPasskey: "보안 키 또는 패스 키" securityKey: "보안 키" @@ -907,7 +911,8 @@ unmuteThread: "글타래 뮤트 해제" followingVisibility: "팔로우의 공개 범위" followersVisibility: "팔로워의 공개 범위" continueThread: "글타래 더 보기" -deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? " +deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까?" +deleteAccountConfirmAndWarn: "계정이 삭제됩니다.\n삭제 요청 후 다시 로그인하면 계정 삭제가 중단되어 버립니다.\n계속하시겠습니까?" incorrectPassword: "비밀번호가 올바르지 않습니다." voteConfirm: "\"{choice}\"에 투표하시겠습니까?" hide: "숨기기" @@ -1085,6 +1090,7 @@ audioFiles: "소리" dataSaver: "데이터 절약 모드" accountMigration: "계정 이동" accountMoved: "이 사용자는 다음 계정으로 이사했습니다:" +accountMovedFrom: "이 사용자는 다음 계정에서 이사했습니다:" accountMovedShort: "이사한 계정입니다" operationForbidden: "사용할 수 없습니다" forceShowAds: "광고를 항상 표시" @@ -1798,6 +1804,7 @@ _accountDelete: requestAccountDelete: "계정 삭제 요청" started: "삭제 작업이 시작되었습니다." inProgress: "삭제 진행 중" + dontLogin: "삭제가 중단되어 버릴 수 있으므로, 계정에 로그인하지 않는 것을 권장합니다." _ad: back: "뒤로" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" From 1367c8dde6d2172ff10140a7593628d119b5f287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 03:29:44 +0900 Subject: [PATCH 02/10] =?UTF-8?q?fix(backend/test):=20MisskeyIO#727=20?= =?UTF-8?q?=E4=BB=A5=E9=99=8De2e=E3=83=86=E3=82=B9=E3=83=88=E3=81=8C?= =?UTF-8?q?=E3=81=9F=E3=81=BE=E3=81=AB=E5=A4=B1=E6=95=97=E3=81=99=E3=82=8B?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(MisskeyIO#735)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/NoteCreateService.ts | 2 +- packages/backend/src/queue/processors/InboxProcessorService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 613620840..66acd6b22 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -219,7 +219,7 @@ export class NoteCreateService implements OnApplicationShutdown { private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('note:create'); - this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount); + this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount); } @bindThis diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 3b9a479a6..259fd06f6 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -51,7 +51,7 @@ export class InboxProcessorService implements OnApplicationShutdown { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('inbox'); - this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance); + this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance); } @bindThis From 454a3f91fabd310c829ce9e7f4e2b7ee8977680f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 03:46:58 +0900 Subject: [PATCH 03/10] Use DragonflyDB (MisskeyIO#716) --- .config/docker_example.yml | 8 +++---- .devcontainer/devcontainer.yml | 8 +++---- .devcontainer/docker-compose.yml | 24 +++++++++++++------- .dockerignore | 1 - .github/workflows/test-backend.yml | 28 ++++++++++++++++++++---- .gitignore | 1 - chart/templates/Deployment.yml | 21 ++++++++++++++++-- docker-compose.local-db.yml | 22 +++++++++++++------ docker-compose_example.yml | 26 ++++++++++++++-------- packages/backend/test/docker-compose.yml | 15 +++++++++++-- 10 files changed, 112 insertions(+), 42 deletions(-) diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 8c41261f6..d80516ce2 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -72,7 +72,7 @@ dbReplications: false #───┘ Redis configuration └───────────────────────────────────── redis: - host: keydb + host: dragonfly port: 6379 #family: 0 # 0=Both, 4=IPv4, 6=IPv6 #pass: example-pass @@ -80,7 +80,7 @@ redis: #db: 1 #redisForPubsub: -# host: keydb +# host: dragonfly # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass @@ -88,7 +88,7 @@ redis: # #db: 1 #redisForJobQueue: -# host: keydb +# host: dragonfly # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass @@ -96,7 +96,7 @@ redis: # #db: 1 #redisForTimelines: -# host: keydb +# host: dragonfly # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 38dbb0bb6..baf1d3381 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -72,7 +72,7 @@ dbReplications: false #───┘ Redis configuration └───────────────────────────────────── redis: - host: keydb + host: dragonfly port: 6379 #family: 0 # 0=Both, 4=IPv4, 6=IPv6 #pass: example-pass @@ -80,7 +80,7 @@ redis: #db: 1 #redisForPubsub: -# host: keydb +# host: dragonfly # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass @@ -88,7 +88,7 @@ redis: # #db: 1 #redisForJobQueue: -# host: keydb +# host: dragonfly # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass @@ -96,7 +96,7 @@ redis: # #db: 1 #redisForTimelines: -# host: keydb +# host: dragonfly # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index ece9e8f11..d9f38b823 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -15,17 +15,25 @@ services: - internal_network - external_network - keydb: + dragonfly: restart: unless-stopped - image: eqalpha/keydb:latest + image: docker.dragonflydb.io/dragonflydb/dragonfly + ulimits: + memlock: -1 + environment: + DFLY_snapshot_cron: '* * * * *' + DFLY_version_check: false + DFLY_tcp_backlog: 2048 + DFLY_default_lua_flags: allow-undeclared-keys + DFLY_pipeline_squash: 0 + DFLY_multi_exec_squash: false + DFLY_conn_io_threads: 4 + DFLY_epoll_file_threads: 4 + DFLY_proactor_threads: 4 networks: - internal_network volumes: - - keydb-data:/data - healthcheck: - test: "keydb-cli ping" - interval: 5s - retries: 20 + - dragonfly-data:/data db: restart: unless-stopped @@ -45,7 +53,7 @@ services: volumes: postgres-data: - keydb-data: + dragonfly-data: networks: internal_network: diff --git a/.dockerignore b/.dockerignore index 78d119f94..087e9861e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,7 +11,6 @@ docker-compose.yml node_modules/ packages/*/node_modules redis/ -keydb/ files/ fluent-emojis/ .pnp.* diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index d6a07299b..1326f37f2 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -32,8 +32,18 @@ jobs: env: POSTGRES_DB: test-misskey POSTGRES_HOST_AUTH_METHOD: trust - keydb: - image: eqalpha/keydb:latest + dragonfly: + image: docker.dragonflydb.io/dragonflydb/dragonfly + options: --ulimit "memlock=-1" + env: + DFLY_version_check: false + DFLY_tcp_backlog: 2048 + DFLY_default_lua_flags: allow-undeclared-keys + DFLY_pipeline_squash: 0 + DFLY_multi_exec_squash: false + DFLY_conn_io_threads: 4 + DFLY_epoll_file_threads: 4 + DFLY_proactor_threads: 4 ports: - 56312:6379 @@ -84,8 +94,18 @@ jobs: env: POSTGRES_DB: test-misskey POSTGRES_HOST_AUTH_METHOD: trust - keydb: - image: eqalpha/keydb:latest + dragonfly: + image: docker.dragonflydb.io/dragonflydb/dragonfly + options: --ulimit "memlock=-1" + env: + DFLY_version_check: false + DFLY_tcp_backlog: 2048 + DFLY_default_lua_flags: allow-undeclared-keys + DFLY_pipeline_squash: 0 + DFLY_multi_exec_squash: false + DFLY_conn_io_threads: 4 + DFLY_epoll_file_threads: 4 + DFLY_proactor_threads: 4 ports: - 56312:6379 diff --git a/.gitignore b/.gitignore index 644fe56d6..c417e9865 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,6 @@ run.bat api-docs.json *.log /redis -/keydb *.code-workspace .DS_Store /files diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml index a6ea8b216..2d3c89e67 100644 --- a/chart/templates/Deployment.yml +++ b/chart/templates/Deployment.yml @@ -37,8 +37,25 @@ spec: value: "misskey" ports: - containerPort: 5432 - - name: keydb - image: eqalpha/keydb:latest + - name: dragonfly + image: docker.dragonflydb.io/dragonflydb/dragonfly + env: + - name: DFLY_version_check + value: false + - name: DFLY_tcp_backlog + value: 2048 + - name: DFLY_default_lua_flags + value: allow-undeclared-keys + - name: DFLY_pipeline_squash + value: 0 + - name: DFLY_multi_exec_squash + value: false + - name: DFLY_conn_io_threads + value: 4 + - name: DFLY_epoll_file_threads + value: 4 + - name: DFLY_proactor_threads + value: 4 ports: - containerPort: 6379 volumes: diff --git a/docker-compose.local-db.yml b/docker-compose.local-db.yml index f758e03b6..25793bc4f 100644 --- a/docker-compose.local-db.yml +++ b/docker-compose.local-db.yml @@ -3,17 +3,25 @@ version: "3" # このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します services: - keydb: + dragonfly: restart: always - image: eqalpha/keydb:latest + image: docker.dragonflydb.io/dragonflydb/dragonfly + ulimits: + memlock: -1 + environment: + DFLY_snapshot_cron: '* * * * *' + DFLY_version_check: false + DFLY_tcp_backlog: 2048 + DFLY_default_lua_flags: allow-undeclared-keys + DFLY_pipeline_squash: 0 + DFLY_multi_exec_squash: false + DFLY_conn_io_threads: 4 + DFLY_epoll_file_threads: 4 + DFLY_proactor_threads: 4 ports: - "6379:6379" volumes: - - ./keydb:/data - healthcheck: - test: "keydb-cli ping" - interval: 5s - retries: 20 + - ./redis:/data db: restart: always diff --git a/docker-compose_example.yml b/docker-compose_example.yml index aad722730..379bc3d77 100644 --- a/docker-compose_example.yml +++ b/docker-compose_example.yml @@ -6,13 +6,13 @@ services: restart: always links: - db - - keydb + - dragonfly # - mcaptcha # - meilisearch depends_on: db: condition: service_healthy - keydb: + dragonfly: condition: service_healthy ports: - "3000:3000" @@ -23,17 +23,25 @@ services: - ./files:/misskey/files - ./.config:/misskey/.config:ro - keydb: + dragonfly: restart: always - image: eqalpha/keydb:latest + image: docker.dragonflydb.io/dragonflydb/dragonfly + ulimits: + memlock: -1 + environment: + DFLY_snapshot_cron: '* * * * *' + DFLY_version_check: false + DFLY_tcp_backlog: 2048 + DFLY_default_lua_flags: allow-undeclared-keys + DFLY_pipeline_squash: 0 + DFLY_multi_exec_squash: false + DFLY_conn_io_threads: 4 + DFLY_epoll_file_threads: 4 + DFLY_proactor_threads: 4 networks: - internal_network volumes: - - ./keydb:/data - healthcheck: - test: "keydb-cli ping" - interval: 5s - retries: 20 + - ./redis:/data db: restart: always diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml index 286a6607a..74659a952 100644 --- a/packages/backend/test/docker-compose.yml +++ b/packages/backend/test/docker-compose.yml @@ -1,8 +1,19 @@ version: "3" services: - keydbtest: - image: eqalpha/keydb:latest + dragonflytest: + image: docker.dragonflydb.io/dragonflydb/dragonfly + ulimits: + memlock: -1 + environment: + DFLY_version_check: false + DFLY_tcp_backlog: 2048 + DFLY_default_lua_flags: allow-undeclared-keys + DFLY_pipeline_squash: 0 + DFLY_multi_exec_squash: false + DFLY_conn_io_threads: 4 + DFLY_epoll_file_threads: 4 + DFLY_proactor_threads: 4 ports: - "127.0.0.1:56312:6379" From 968901e73dc9261117725e2c2bfcc590f959015c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:07:18 +0900 Subject: [PATCH 04/10] =?UTF-8?q?enhance(backend):=20=E5=87=8D=E7=B5=90?= =?UTF-8?q?=E3=81=AE=E5=BE=8C=E5=87=A6=E7=90=86=E3=82=92Queue=E3=81=A7?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=20(MisskeyIO#733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/QueueService.ts | 10 ++ .../backend/src/core/UserSuspendService.ts | 68 +----------- .../backend/src/queue/QueueProcessorModule.ts | 2 + .../src/queue/QueueProcessorService.ts | 3 + .../DeleteAccountProcessorService.ts | 2 +- .../processors/UserSuspendProcessorService.ts | 101 ++++++++++++++++++ packages/backend/src/queue/types.ts | 4 + 7 files changed, 123 insertions(+), 67 deletions(-) create mode 100644 packages/backend/src/queue/processors/UserSuspendProcessorService.ts diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 7c92dd402..526751044 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -364,6 +364,16 @@ export class QueueService { }); } + @bindThis + public createUserSuspendJob(user: ThinUser) { + return this.dbQueue.add('userSuspend', { + user: { id: user.id }, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + @bindThis public createReportAbuseJob(report: MiAbuseUserReport) { return this.dbQueue.add('reportAbuse', report); diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 677db309d..2cbc35d93 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -9,16 +9,7 @@ import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import type { MiUser } from '@/models/User.js'; -import type { - AntennasRepository, - ClipNotesRepository, - ClipsRepository, - FollowingsRepository, - FollowRequestsRepository, - UserListMembershipsRepository, - UserListsRepository, - WebhooksRepository, -} from '@/models/_.js'; +import type { FollowingsRepository } from '@/models/_.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -36,27 +27,6 @@ export class UserSuspendService { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - @Inject(DI.followRequestsRepository) - private followRequestsRepository: FollowRequestsRepository, - - @Inject(DI.antennasRepository) - private antennasRepository: AntennasRepository, - - @Inject(DI.webhooksRepository) - private webhooksRepository: WebhooksRepository, - - @Inject(DI.userListsRepository) - private userListsRepository: UserListsRepository, - - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - - @Inject(DI.userListMembershipsRepository) - private userListMembershipsRepository: UserListMembershipsRepository, - private queueService: QueueService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, @@ -72,41 +42,7 @@ export class UserSuspendService { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); - const promises: Promise[] = []; - - let cursor = ''; - while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition - const clipNotes = await this.clipNotesRepository.createQueryBuilder('c') - .select('c.id') - .innerJoin('c.note', 'n') - .where('n.userId = :userId', { userId: user.id }) - .andWhere('c.id > :cursor', { cursor }) - .orderBy('c.id', 'ASC') - .limit(500) - .getRawMany<{ id: string }>(); - - if (clipNotes.length === 0) break; - - cursor = clipNotes.at(-1)?.id ?? ''; - - promises.push(this.clipNotesRepository.createQueryBuilder() - .delete() - .where('id IN (:...ids)', { ids: clipNotes.map((clipNote) => clipNote.id) }) - .execute()); - } - - await Promise.allSettled([ - this.followRequestsRepository.delete({ followeeId: user.id }), - this.followRequestsRepository.delete({ followerId: user.id }), - - this.antennasRepository.delete({ userId: user.id }), - this.webhooksRepository.delete({ userId: user.id }), - this.userListsRepository.delete({ userId: user.id }), - this.clipsRepository.delete({ userId: user.id }), - - ...promises, - this.userListMembershipsRepository.delete({ userId: user.id }), - ]); + await this.queueService.createUserSuspendJob(user); if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにDelete配信 diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 8b9e5a45b..f3588544f 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -16,6 +16,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; +import { UserSuspendProcessorService } from './processors/UserSuspendProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; @@ -68,6 +69,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor ImportUserListsProcessorService, ImportCustomEmojisProcessorService, ImportAntennasProcessorService, + UserSuspendProcessorService, DeleteAccountProcessorService, DeleteFileProcessorService, CleanRemoteFilesProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 540a58b8b..738ba9e25 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -28,6 +28,7 @@ import { ImportBlockingProcessorService } from './processors/ImportBlockingProce import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js'; import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js'; +import { UserSuspendProcessorService } from './processors/UserSuspendProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; @@ -106,6 +107,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private importUserListsProcessorService: ImportUserListsProcessorService, private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService, private importAntennasProcessorService: ImportAntennasProcessorService, + private userSuspendProcessorService: UserSuspendProcessorService, private deleteAccountProcessorService: DeleteAccountProcessorService, private deleteFileProcessorService: DeleteFileProcessorService, private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService, @@ -184,6 +186,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); case 'importAntennas': return this.importAntennasProcessorService.process(job); case 'deleteAccount': return this.deleteAccountProcessorService.process(job); + case 'userSuspend': return this.userSuspendProcessorService.process(job); case 'reportAbuse': return this.reportAbuseProcessorService.process(job); default: throw new Error(`unrecognized job type ${job.name} for db`); } diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 08dc31e54..84e949d4f 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -40,7 +40,7 @@ export class DeleteAccountProcessorService { private roleService: RoleService, private queueLoggerService: QueueLoggerService, ) { - this.logger = this.queueLoggerService.logger.createSubLogger('delete-account'); + this.logger = this.queueLoggerService.logger.createSubLogger('account:delete'); } private async deleteNotes(user: MiUser) { diff --git a/packages/backend/src/queue/processors/UserSuspendProcessorService.ts b/packages/backend/src/queue/processors/UserSuspendProcessorService.ts new file mode 100644 index 000000000..9f4c2576a --- /dev/null +++ b/packages/backend/src/queue/processors/UserSuspendProcessorService.ts @@ -0,0 +1,101 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; +import { DI } from '@/di-symbols.js'; +import type Logger from '@/logger.js'; +import type { + AntennasRepository, + ClipNotesRepository, + ClipsRepository, + FollowRequestsRepository, + UserListMembershipsRepository, + UserListsRepository, UsersRepository, + WebhooksRepository, +} from '@/models/_.js'; +import { QueueLoggerService } from '@/queue/QueueLoggerService.js'; +import type * as Bull from "bullmq"; +import type { DbUserSuspendJobData } from "@/queue/types.js"; + +@Injectable() +export class UserSuspendProcessorService { + public logger: Logger; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, + + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('account:suspend'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + this.logger.warn(`Cleaning up suspended account of ${job.data.user.id} ...`, { userSuspendJobData: job.data }); + + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + return 'User not found'; + } + + const promises: Promise[] = []; + + let cursor = ''; + while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition + const clipNotes = await this.clipNotesRepository.createQueryBuilder('c') + .select('c.id') + .innerJoin('c.note', 'n') + .where('n.userId = :userId', { userId: user.id }) + .andWhere('c.id > :cursor', { cursor }) + .orderBy('c.id', 'ASC') + .limit(100) + .getRawMany<{ id: string }>(); + + if (clipNotes.length === 0) break; + + cursor = clipNotes.at(-1)?.id ?? ''; + + promises.push(this.clipNotesRepository.createQueryBuilder() + .delete() + .where('id IN (:...ids)', { ids: clipNotes.map((clipNote) => clipNote.id) }) + .execute()); + } + + await Promise.allSettled([ + this.followRequestsRepository.delete({ followeeId: user.id }), + this.followRequestsRepository.delete({ followerId: user.id }), + + this.antennasRepository.delete({ userId: user.id }), + this.webhooksRepository.delete({ userId: user.id }), + this.userListsRepository.delete({ userId: user.id }), + this.clipsRepository.delete({ userId: user.id }), + + ...promises, + this.userListMembershipsRepository.delete({ userId: user.id }), + ]); + + this.logger.info(`Completed cleaning up suspended account of ${job.data.user.id}`); + + return 'done'; + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 954168964..49aedec58 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -83,6 +83,10 @@ export type DbUserDeleteJobData = { onlyFiles?: boolean; }; +export type DbUserSuspendJobData = { + user: ThinUser +}; + export type DbUserImportJobData = { user: ThinUser; fileId: MiDriveFile['id']; From 46e4b8f8e1a7ba77c12360e62623975e3d0f1151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:07:37 +0900 Subject: [PATCH 05/10] =?UTF-8?q?fix(api/users):=20=E3=83=9A=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=83=93=E3=83=A5=E3=83=BC=E3=81=AE=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=81=AE=E5=8F=96=E5=BE=97=E7=AF=84=E5=9B=B2=E3=81=8C?= =?UTF-8?q?=E7=8B=AD=E3=81=99=E3=81=8E=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(MisskeyIO#734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/chart/charts/per-user-pv.ts | 31 ++++++++++-- packages/backend/src/core/chart/core.ts | 47 ++----------------- .../backend/src/server/api/endpoints/users.ts | 32 ++++++------- 3 files changed, 45 insertions(+), 65 deletions(-) diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts index 02baa5ec1..3ad90b090 100644 --- a/packages/backend/src/core/chart/charts/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/per-user-pv.ts @@ -7,6 +7,7 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import type { MiUser } from '@/models/User.js'; import { AppLockService } from '@/core/AppLockService.js'; +import { addTime, dateUTC, subtractTime } from '@/misc/prelude/time.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; @@ -54,10 +55,30 @@ export default class PerUserPvChart extends Chart { // eslint-dis } @bindThis - public async getChartUsers(span: 'hour' | 'day', order: 'ASC' | 'DESC', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{ - userId: string; - count: number; -}[]> { - return await this.getChartPv(span, amount, cursor, limit, offset, order); + public async getUsersRanking(span: 'hour' | 'day', order: 'ASC' | 'DESC', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{ userId: string; count: number; }[]> { + const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate(); + const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never; + + const lt = dateUTC([y, m, d, h, _m, _s, _ms]); + + const gt = + span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') : + span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') : + new Error('not happen') as never; + + const repository = + span === 'hour' ? this.repositoryForHour : + span === 'day' ? this.repositoryForDay : + new Error('not happen') as never; + + // ログ取得 + return await repository.createQueryBuilder() + .select('"group" as "userId", sum("___upv_user" + "___upv_visitor") as "count"') + .where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) }) + .groupBy('"userId"') + .orderBy('"count"', order) + .offset(offset) + .limit(limit) + .getRawMany<{ userId: string, count: number }>(); } } diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index 3874c1e14..09757c887 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -147,10 +147,8 @@ export default abstract class Chart { // ↓にしたいけどfindOneとかで型エラーになる //private repositoryForHour: Repository>; //private repositoryForDay: Repository>; - private repositoryForHour: Repository<{ id: number; group?: string | null; date: number;}>; - private repositoryForDay: Repository<{ id: number; group?: string | null; date: number;}>; - private repositoryUserPvForHour: Repository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>; - private repositoryUserPvForDay: Repository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>; + protected repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>; + protected repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>; /** * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用) */ @@ -186,11 +184,11 @@ export default abstract class Chart { return columns; } - private static dateToTimestamp(x: Date): number { + protected static dateToTimestamp(x: Date): number { return Math.floor(x.getTime() / 1000); } - private static parseDate(date: Date): [number, number, number, number, number, number, number] { + protected static parseDate(date: Date): [number, number, number, number, number, number, number] { const y = date.getUTCFullYear(); const m = date.getUTCMonth(); const d = date.getUTCDate(); @@ -202,7 +200,7 @@ export default abstract class Chart { return [y, m, d, h, _m, _s, _ms]; } - private static getCurrentDate() { + protected static getCurrentDate() { return Chart.parseDate(new Date()); } @@ -274,8 +272,6 @@ export default abstract class Chart { const { hour, day } = Chart.schemaToEntity(name, schema, grouped); this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour); this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day); - this.repositoryUserPvForHour = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(hour); - this.repositoryUserPvForDay = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(day); } @bindThis @@ -725,37 +721,4 @@ export default abstract class Chart { } return object as Unflatten>; } - - @bindThis - public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number, order: 'ASC' | 'DESC'): Promise< - { - userId: string, - count: number, - }[] - > { - const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate(); - const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never; - - const lt = dateUTC([y, m, d, h, _m, _s, _ms]); - - const gt = - span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') : - span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') : - new Error('not happen') as never; - - const repository = - span === 'hour' ? this.repositoryUserPvForHour : - span === 'day' ? this.repositoryUserPvForDay : - new Error('not happen') as never; - - // ログ取得 - return await repository.createQueryBuilder() - .select('"group" as "userId", sum("___upv_user" + "___upv_visitor") as "count"') - .where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) }) - .groupBy('"userId"') - .orderBy('"count"', order) - .offset(offset) - .limit(limit) - .getRawMany<{ userId: string, count: number }>(); - } } diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index 8adbc7b28..8c02b9ed5 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -72,12 +72,16 @@ export default class extends Endpoint { // eslint- if (ps.hostname) { query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); } - const chartUsers: { userId: string; count: number; }[] = []; + + let pvUsers: { userId: string; count: number; }[] | undefined = undefined; if (ps.sort?.endsWith('pv')) { - await this.perUserPvChart.getChartUsers('hour', ps.sort === '+pv' ? 'DESC' : 'ASC', 0, null, ps.limit, ps.offset).then(users => { - chartUsers.push(...users); - }); + // 直近12時間のPVランキングを取得 + pvUsers = await this.perUserPvChart.getUsersRanking( + 'hour', ps.sort.startsWith('+') ? 'DESC' : 'ASC', + 12, null, ps.limit, ps.offset, + ); } + switch (ps.sort) { case '+follower': query.orderBy('user.followersCount', 'DESC'); break; case '-follower': query.orderBy('user.followersCount', 'ASC'); break; @@ -85,16 +89,8 @@ export default class extends Endpoint { // eslint- case '-createdAt': query.orderBy('user.id', 'ASC'); break; case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break; case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break; - case '+pv': - if (chartUsers.length > 0) { - query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) }); - } - break; - case '-pv': - if (chartUsers.length > 0) { - query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) }); - } - break; + case '+pv': query.andWhere('user.id IN (:...userIds)', { userIds: pvUsers?.map(user => user.userId) ?? [] }); break; + case '-pv': query.andWhere('user.id IN (:...userIds)', { userIds: pvUsers?.map(user => user.userId) ?? [] }); break; default: query.orderBy('user.id', 'ASC'); break; } @@ -107,14 +103,14 @@ export default class extends Endpoint { // eslint- const users = await query.getMany(); if (ps.sort === '+pv') { users.sort((a, b) => { - const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0; - const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0; + const aPv = pvUsers?.find(u => u.userId === a.id)?.count ?? 0; + const bPv = pvUsers?.find(u => u.userId === b.id)?.count ?? 0; return bPv - aPv; }); } else if (ps.sort === '-pv') { users.sort((a, b) => { - const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0; - const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0; + const aPv = pvUsers?.find(u => u.userId === a.id)?.count ?? 0; + const bPv = pvUsers?.find(u => u.userId === b.id)?.count ?? 0; return aPv - bPv; }); } From 8e9b6dc4d18825c98975cd5b424cdfd5a817f2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:08:44 +0900 Subject: [PATCH 06/10] =?UTF-8?q?enhance(backend):=20=E5=80=8B=E4=BA=BA?= =?UTF-8?q?=E5=AE=9B=E3=81=AE=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B=E3=81=AF?= =?UTF-8?q?=E3=82=8F=E3=81=8B=E3=81=A3=E3=81=9F=E3=82=92=E6=8A=BC=E3=81=99?= =?UTF-8?q?=E3=81=A8=E9=81=8E=E5=8E=BB=E3=81=AE=E3=81=8A=E7=9F=A5=E3=82=89?= =?UTF-8?q?=E3=81=9B=E3=81=AB=E8=A1=A8=E7=A4=BA=E3=81=95=E3=81=9B=E3=82=8B?= =?UTF-8?q?=20(MisskeyIO#736)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/AnnouncementService.ts | 50 +++++++++---------- .../api/endpoints/admin/announcements/list.ts | 6 +++ .../MkUserAnnouncementEditDialog.vue | 3 +- packages/frontend/src/pages/admin-user.vue | 2 +- .../src/pages/admin/announcements.vue | 2 +- packages/misskey-js/src/autogen/types.ts | 2 + 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index 394b47c99..07a8a7833 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -138,7 +138,7 @@ export class AnnouncementService { limit: number, offset: number, moderator: MiUser, - ): Promise<(MiAnnouncement & { userInfo: Packed<'UserLite'> | null, reads: number })[]> { + ): Promise<(MiAnnouncement & { userInfo: Packed<'UserLite'> | null, reads: number, lastReadAt: Date | null })[]> { const query = this.announcementsRepository.createQueryBuilder('announcement'); if (userId) { @@ -157,13 +157,14 @@ export class AnnouncementService { .offset(offset) .getMany(); - const reads = new Map(); - - for (const announcement of announcements) { - reads.set(announcement, await this.announcementReadsRepository.countBy({ - announcementId: announcement.id, - })); - } + const reads = announcements.length > 0 + ? await this.announcementReadsRepository.createQueryBuilder() + .select('"announcementId", count(*) as "reads", max("id") as "lastReadId"') + .where('"announcementId" IN (:...announcementIds)', { announcementIds: announcements.map(a => a.id) }) + .groupBy('"announcementId"') + .getRawMany<{ announcementId: string, reads: number, lastReadId: string | null }>() + .then(rs => new Map(rs.map(r => [r.announcementId, { reads: r.reads, lastReadAt: r.lastReadId ? this.idService.parse(r.lastReadId).date : null }]))) + : new Map(); const users = await this.usersRepository.findBy({ id: In(announcements.map(a => a.userId).filter(id => id != null)), @@ -174,8 +175,8 @@ export class AnnouncementService { return announcements.map(announcement => ({ ...announcement, + ...reads.get(announcement.id) ?? { reads: 0, lastReadAt: null }, userInfo: packedUsers.find(u => u.id === announcement.userId) ?? null, - reads: reads.get(announcement) ?? 0, })); } @@ -293,18 +294,20 @@ export class AnnouncementService { 'read.id IS NOT NULL as "isRead"', ]); query - .andWhere( - new Brackets((qb) => { - qb.orWhere('announcement."userId" = :userId', { userId: me.id }); - qb.orWhere('announcement."userId" IS NULL'); - }), - ) - .andWhere( - new Brackets((qb) => { - qb.orWhere('announcement."forExistingUsers" = false'); - qb.orWhere('announcement.id > :userId', { userId: me.id }); - }), - ); + .andWhere(new Brackets((qb) => { + qb.orWhere(new Brackets((nqb) => { + nqb.andWhere('announcement."userId" = :userId', { userId: me.id }); + nqb.andWhere(isActive ? 'read.id IS NULL' : 'read.id IS NOT NULL'); + })); + qb.orWhere(new Brackets((nqb) => { + nqb.andWhere('announcement."userId" IS NULL'); + nqb.andWhere('announcement."isActive" = :isActive', { isActive }); + })); + })) + .andWhere(new Brackets((qb) => { + qb.orWhere('announcement."forExistingUsers" = false'); + qb.orWhere('announcement.id > :userId', { userId: me.id }); + })); } else { query.select([ 'announcement.*', @@ -312,12 +315,9 @@ export class AnnouncementService { ]); query.andWhere('announcement."userId" IS NULL'); query.andWhere('announcement."forExistingUsers" = false'); + query.andWhere('announcement."isActive" = :isActive', { isActive }); } - query.andWhere('announcement."isActive" = :isActive', { - isActive: isActive, - }); - if (isActive) { query.orderBy({ '"isRead"': 'ASC', diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index c8719ffb8..573652ac5 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -97,6 +97,11 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + lastReadAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, }, }, }, @@ -140,6 +145,7 @@ export default class extends Endpoint { // eslint- userId: announcement.userId, user: announcement.userInfo, reads: announcement.reads, + lastReadAt: announcement.lastReadAt?.toISOString() ?? null, })); }); } diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index ec46dac34..7fee7c1b1 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.silence }} -

{{ i18n.tsx.nUsersRead({ n: reads }) }}

+

{{ i18n.tsx.nUsersRead({ n: reads }) }} ()

{{ i18n.ts.delete }} @@ -94,6 +94,7 @@ const closeDuration = ref(props.announcement ? props.announcement.closeD const displayOrder = ref(props.announcement ? props.announcement.displayOrder : 0); const silence = ref(props.announcement ? props.announcement.silence : false); const reads = ref(props.announcement ? props.announcement.reads : 0); +const lastReadAt = ref(props.announcement ? props.announcement.lastReadAt : null); const emit = defineEmits<{ (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 3a9a6c775..4f339e550 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ announcement.title }} - {{ i18n.ts.messageRead }} + {{ i18n.ts.messageRead }} () diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index afd97bc06..7a0f12844 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.silence }} -

{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}

+

{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }} ()

{{ i18n.ts.specifyUser }}
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 92b3b7769..ae000aca0 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -6183,6 +6183,8 @@ export type operations = { userId: string | null; user: components['schemas']['UserLite'] | null; reads: number; + /** Format: date-time */ + lastReadAt: string | null; })[]; }; }; From c82bf7583ad2f378e6f2d00e48e8d2bcfcf569e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:21:28 +0900 Subject: [PATCH 07/10] =?UTF-8?q?fix(api/users):=20PV=E3=83=A9=E3=83=B3?= =?UTF-8?q?=E3=82=AD=E3=83=B3=E3=82=B0=E3=81=AB=E8=AA=B0=E3=82=82=E3=81=84?= =?UTF-8?q?=E3=81=AA=E3=81=84=E5=A0=B4=E5=90=88=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=81=AA=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20(MisskeyIO#737)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/server/api/endpoints/users.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index 8c02b9ed5..3ba4f9e3f 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -73,10 +73,10 @@ export default class extends Endpoint { // eslint- query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); } - let pvUsers: { userId: string; count: number; }[] | undefined = undefined; + let pvRankedUsers: { userId: string; count: number; }[] | undefined = undefined; if (ps.sort?.endsWith('pv')) { // 直近12時間のPVランキングを取得 - pvUsers = await this.perUserPvChart.getUsersRanking( + pvRankedUsers = await this.perUserPvChart.getUsersRanking( 'hour', ps.sort.startsWith('+') ? 'DESC' : 'ASC', 12, null, ps.limit, ps.offset, ); @@ -89,8 +89,8 @@ export default class extends Endpoint { // eslint- case '-createdAt': query.orderBy('user.id', 'ASC'); break; case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break; case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break; - case '+pv': query.andWhere('user.id IN (:...userIds)', { userIds: pvUsers?.map(user => user.userId) ?? [] }); break; - case '-pv': query.andWhere('user.id IN (:...userIds)', { userIds: pvUsers?.map(user => user.userId) ?? [] }); break; + case '+pv': query.andWhere((pvRankedUsers?.length ?? 0) > 0 ? 'user.id IN (:...userIds)' : '1 = 0', { userIds: pvRankedUsers?.map(user => user.userId) ?? [] }); break; + case '-pv': query.andWhere((pvRankedUsers?.length ?? 0) > 0 ? 'user.id IN (:...userIds)' : '1 = 0', { userIds: pvRankedUsers?.map(user => user.userId) ?? [] }); break; default: query.orderBy('user.id', 'ASC'); break; } @@ -103,14 +103,14 @@ export default class extends Endpoint { // eslint- const users = await query.getMany(); if (ps.sort === '+pv') { users.sort((a, b) => { - const aPv = pvUsers?.find(u => u.userId === a.id)?.count ?? 0; - const bPv = pvUsers?.find(u => u.userId === b.id)?.count ?? 0; + const aPv = pvRankedUsers?.find(u => u.userId === a.id)?.count ?? 0; + const bPv = pvRankedUsers?.find(u => u.userId === b.id)?.count ?? 0; return bPv - aPv; }); } else if (ps.sort === '-pv') { users.sort((a, b) => { - const aPv = pvUsers?.find(u => u.userId === a.id)?.count ?? 0; - const bPv = pvUsers?.find(u => u.userId === b.id)?.count ?? 0; + const aPv = pvRankedUsers?.find(u => u.userId === a.id)?.count ?? 0; + const bPv = pvRankedUsers?.find(u => u.userId === b.id)?.count ?? 0; return aPv - bPv; }); } From 69f31e624699d92f563e89cedf135afe5edc8e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:00:49 +0900 Subject: [PATCH 08/10] =?UTF-8?q?enhance(frontend):=20=E5=AE=9A=E7=BE=A9?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=82=82=E8=A1=A8=E7=A4=BA=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(MisskeyIO#738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit +登録時のAPIエラーを表示できるように --- .../src/components/MkSignupDialog.form.vue | 38 +++--- packages/frontend/src/os.ts | 111 ++++++++++-------- packages/frontend/src/scripts/misskey-api.ts | 9 +- 3 files changed, 84 insertions(+), 74 deletions(-) diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 52d910afc..4c923de37 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -189,7 +189,7 @@ async function onSubmit(): Promise { submitting.value = true; try { - await misskeyApi('signup', { + await os.apiWithDialog('signup', { username: username.value, password: password.value.password, emailAddress: email.value, @@ -198,35 +198,27 @@ async function onSubmit(): Promise { 'm-captcha-response': mCaptchaResponse.value, 'g-recaptcha-response': reCaptchaResponse.value, 'turnstile-response': turnstileResponse.value, - }); - if (instance.emailRequiredForSignup) { - os.alert({ - type: 'success', - title: i18n.ts._signup.almostThere, - text: i18n.tsx._signup.emailSent({ email: email.value }), - }); - emit('signupEmailPending'); - } else { - const res = await misskeyApi('signin', { - username: username.value, - password: password.value.password, - }); - emit('signup', res); + }, undefined, (res) => { + if (instance.emailRequiredForSignup) { + os.alert({ + type: 'success', + title: i18n.ts._signup.almostThere, + text: i18n.tsx._signup.emailSent({ email: email.value }), + }); + emit('signupEmailPending'); + } else { + emit('signup', { id: res.id, i: res.token }); - if (props.autoSet) { - return login(res.i); + if (props.autoSet) { + login(res.token); + } } - } + }); } catch { submitting.value = false; hcaptcha.value?.reset?.(); recaptcha.value?.reset?.(); turnstile.value?.reset?.(); - - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); } } diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index b4e8bf3f8..5560335d8 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -34,60 +34,77 @@ export const apiWithDialog = ( { + onSuccess?: ((res: Misskey.api.SwitchCaseResponseType) => void) | null | undefined, + onFailure?: ((err: Misskey.api.APIError) => void) | null, +): Promise> => { const promise = misskeyApi(endpoint, data, token); - promiseDialog(promise, null, async (err) => { - let title: string | undefined; - let text = err.message + '\n' + err.id; - if (err.code === 'INTERNAL_ERROR') { - title = i18n.ts.internalServerError; - text = i18n.ts.internalServerErrorDescription; - const date = new Date().toISOString(); - const { result } = await actions({ - type: 'error', - title, - text, - details: err.info, - actions: [{ - value: 'ok', - text: i18n.ts.gotIt, - primary: true, - }, { - value: 'copy', - text: i18n.ts.copyErrorInfo, - }], - }); - if (result === 'copy') { - copyToClipboard(`Endpoint: ${endpoint}\nInfo: ${JSON.stringify(err.info)}\nDate: ${date}`); - success(); - } - return; - } else if (err.code === 'RATE_LIMIT_EXCEEDED') { - title = i18n.ts.cannotPerformTemporary; - text = i18n.ts.cannotPerformTemporaryDescription; - } else if (err.code === 'INVALID_PARAM') { - title = i18n.ts.invalidParamError; - text = i18n.ts.invalidParamErrorDescription; - } else if (err.code === 'ROLE_PERMISSION_DENIED') { - title = i18n.ts.permissionDeniedError; - text = i18n.ts.permissionDeniedErrorDescription; - } else if (err.code.startsWith('TOO_MANY')) { - title = i18n.ts.youCannotCreateAnymore; - text = `${i18n.ts.error}: ${err.id}`; - } else if (err.message.startsWith('Unexpected token')) { - title = i18n.ts.gotInvalidResponseError; - text = i18n.ts.gotInvalidResponseErrorDescription; - } - alert({ + promiseDialog(promise, onSuccess, onFailure ?? (err => apiErrorHandler(err, endpoint))); + + return promise; +}); + +export async function apiErrorHandler(err: Misskey.api.APIError, endpoint?: string): Promise { + let title: string | undefined; + let text = err.message + '\n' + err.id; + + if (err.code === 'INTERNAL_ERROR') { + title = i18n.ts.internalServerError; + text = i18n.ts.internalServerErrorDescription; + const date = new Date().toISOString(); + const { result } = await actions({ type: 'error', title, text, details: err.info, + actions: [{ + value: 'ok', + text: i18n.ts.gotIt, + primary: true, + }, { + value: 'copy', + text: i18n.ts.copyErrorInfo, + }], }); - }); + if (result === 'copy') { + copyToClipboard(`Endpoint: ${endpoint}\nInfo: ${JSON.stringify(err.info)}\nDate: ${date}`); + success(); + } + return; + } else if (err.code === 'RATE_LIMIT_EXCEEDED') { + title = i18n.ts.cannotPerformTemporary; + text = i18n.ts.cannotPerformTemporaryDescription; + } else if (err.code === 'INVALID_PARAM') { + title = i18n.ts.invalidParamError; + text = i18n.ts.invalidParamErrorDescription; + } else if (err.code === 'ROLE_PERMISSION_DENIED') { + title = i18n.ts.permissionDeniedError; + text = i18n.ts.permissionDeniedErrorDescription; + } else if (err.code?.startsWith('TOO_MANY')) { + title = i18n.ts.youCannotCreateAnymore; + text = `${i18n.ts.error}: ${err.id}`; + } - return promise; -}) as typeof misskeyApi; + // @ts-expect-error Misskey内部で定義されていない不明なエラー + if (!err.id && (err.statusCode ?? 0) > 499) { + title = i18n.ts.gotInvalidResponseError; + text = i18n.ts.gotInvalidResponseErrorDescription; + } + + if (err.id && !title) { + title = i18n.ts.somethingHappened; + } else if (!title) { + title = i18n.ts.somethingHappened; + text = err.message; + } + + alert({ + type: 'error', + title, + text, + // @ts-expect-error Misskeyのエラーならinfoを、そうでなければそのまま表示 + details: err.id ? err.info : err as unknown, + }); +} export function promiseDialog( promise: Promise, diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts index 49fb6f9e5..641966bea 100644 --- a/packages/frontend/src/scripts/misskey-api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -44,14 +44,15 @@ export function misskeyApi< }, signal, }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { + if (res.ok && res.status !== 204) { + const body = await res.json(); resolve(body); } else if (res.status === 204) { resolve(undefined as _ResT); // void -> undefined } else { - reject(body.error); + // エラー応答で JSON.parse に失敗した場合は HTTP ステータスコードとメッセージを返す + const body = await res.json().catch(() => ({ statusCode: res.status, message: res.statusText })); + reject(typeof body.error === 'object' ? body.error : body); } }).catch(reject); }); From 7cd0c7380d338e4cfe8d4c694ee9f00537a4e188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:01:09 +0900 Subject: [PATCH 09/10] Bump up version to 2024.5.0-io.2f (MisskeyIO#739) --- package.json | 2 +- packages/misskey-js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6d324d829..71c68f37e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.5.0-io.2e", + "version": "2024.5.0-io.2f", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 3e840b5a8..a31628127 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.5.0-io.2e", + "version": "2024.5.0-io.2f", "description": "Misskey SDK for JavaScript", "types": "./built/dts/index.d.ts", "exports": { From 5508d712c45725fa44114b114e1392a33e55f637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:06:45 +0900 Subject: [PATCH 10/10] =?UTF-8?q?Revert=20"enhance(frontend):=20=E3=82=A2?= =?UTF-8?q?=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E5=89=8A=E9=99=A4=E5=89=8D?= =?UTF-8?q?=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0=E3=81=AB=E5=86=8D?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=99=E3=82=8B=E3=81=A8?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=8C=E4=B8=AD=E6=96=AD=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E6=97=A8=E3=81=AE=E8=A1=A8=E8=A8=98=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20(MisskeyIO#726)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2fe5bb0bb39dc43e700201a827e5268ce97f1c4e partially. --- packages/frontend/src/pages/settings/other.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 6d6b8d5c9..2341a57dc 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -114,7 +114,7 @@ async function deleteAccount() { { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.ts.deleteAccountConfirmAndWarn, + text: i18n.ts.deleteAccountConfirm, okWaitInitiate: 'dialog', okWaitDuration: 5, });