summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2025-03-02 18:57:27 +0000
committerdakkar <dakkar@thenautilus.net>2025-03-02 18:57:27 +0000
commit0b5e197afb5f9c2519c22324706c3b27d5d3eea3 (patch)
treec73d3940938e4fd8cc515377f9334c4a48679c7b
parentmerge: pin corepack version (!885) (diff)
parentmerge: Add/fix moderation logs for many endpoints (resolves #911 and #969) (!... (diff)
downloadsharkey-0b5e197afb5f9c2519c22324706c3b27d5d3eea3.tar.gz
sharkey-0b5e197afb5f9c2519c22324706c3b27d5d3eea3.tar.bz2
sharkey-0b5e197afb5f9c2519c22324706c3b27d5d3eea3.zip
Merge branch 'develop' into release/2025.2.2
-rw-r--r--.config/ci.yml59
-rw-r--r--.config/cypress-devcontainer.yml61
-rw-r--r--.config/docker_example.yml63
-rw-r--r--.config/example.yml78
-rw-r--r--.gitlab-ci.yml6
-rw-r--r--.gitlab/merge_request_templates/default.md4
-rw-r--r--CHANGELOG.md102
-rw-r--r--CONTRIBUTING.md83
-rw-r--r--COPYING2
-rw-r--r--Dockerfile4
-rw-r--r--SECURITY.md5
-rw-r--r--chart/files/default.yml38
-rw-r--r--compose_example.yml6
-rw-r--r--locales/ar-SA.yml6
-rw-r--r--locales/bn-BD.yml6
-rw-r--r--locales/ca-ES.yml164
-rw-r--r--locales/cs-CZ.yml6
-rw-r--r--locales/de-DE.yml105
-rw-r--r--locales/en-US.yml108
-rw-r--r--locales/es-ES.yml34
-rw-r--r--locales/fr-FR.yml6
-rw-r--r--locales/id-ID.yml6
-rw-r--r--locales/index.d.ts643
-rw-r--r--locales/it-IT.yml158
-rw-r--r--locales/ja-JP.yml109
-rw-r--r--locales/ja-KS.yml80
-rw-r--r--locales/ko-GS.yml3
-rw-r--r--locales/ko-KR.yml110
-rw-r--r--locales/lo-LA.yml3
-rw-r--r--locales/nl-NL.yml41
-rw-r--r--locales/no-NO.yml3
-rw-r--r--locales/pl-PL.yml6
-rw-r--r--locales/pt-PT.yml133
-rw-r--r--locales/ro-RO.yml3
-rw-r--r--locales/ru-RU.yml16
-rw-r--r--locales/sk-SK.yml6
-rw-r--r--locales/th-TH.yml6
-rw-r--r--locales/tr-TR.yml1
-rw-r--r--locales/uk-UA.yml6
-rw-r--r--locales/uz-UZ.yml6
-rw-r--r--locales/vi-VN.yml6
-rw-r--r--locales/zh-CN.yml158
-rw-r--r--locales/zh-TW.yml148
-rw-r--r--package.json2
-rw-r--r--packages/backend/migration/1709126576000-optimize-emoji-index.js18
-rw-r--r--packages/backend/migration/1731565470048-add-activity-log.js28
-rw-r--r--packages/backend/migration/1731909785724-activity-log-timing.js19
-rw-r--r--packages/backend/migration/1731910422761-rename-activity-log-indexes.js16
-rw-r--r--packages/backend/migration/1731935047347-nullable-activity-log-duration.js20
-rw-r--r--packages/backend/migration/1733756280460-rename_activity_log_to_ap_inbox_log.js32
-rw-r--r--packages/backend/migration/1738043621143-add_user_mandatoryCW.js11
-rw-r--r--packages/backend/migration/1738293576355-create_ap_fetch_log.js19
-rw-r--r--packages/backend/migration/1738346484187-robotsTxt.js16
-rw-r--r--packages/backend/migration/1738446745738-add_user_profile_default_cw.js11
-rw-r--r--packages/backend/migration/1738468079662-add_user_profile_default_cw_priority.js13
-rw-r--r--packages/backend/migration/1739451520729-index_note_attachedFileTypes.js12
-rw-r--r--packages/backend/migration/1739671352784-add_note_processErrors.js11
-rw-r--r--packages/backend/migration/1739671777344-add_user_rejectQuotes.js11
-rw-r--r--packages/backend/migration/1739671847942-add_instance_rejectQuotes.js11
-rw-r--r--packages/backend/package.json6
-rw-r--r--packages/backend/scripts/check_connect.js1
-rw-r--r--packages/backend/src/GlobalModule.ts24
-rw-r--r--packages/backend/src/boot/common.ts2
-rw-r--r--packages/backend/src/boot/entry.ts18
-rw-r--r--packages/backend/src/boot/master.ts47
-rw-r--r--packages/backend/src/config.ts53
-rw-r--r--packages/backend/src/const.ts12
-rw-r--r--packages/backend/src/core/AbuseReportNotificationService.ts32
-rw-r--r--packages/backend/src/core/AccountMoveService.ts21
-rw-r--r--packages/backend/src/core/ApLogService.ts207
-rw-r--r--packages/backend/src/core/CaptchaService.ts332
-rw-r--r--packages/backend/src/core/CoreModule.ts18
-rw-r--r--packages/backend/src/core/CustomEmojiService.ts234
-rw-r--r--packages/backend/src/core/DriveService.ts39
-rw-r--r--packages/backend/src/core/FederatedInstanceService.ts2
-rw-r--r--packages/backend/src/core/FetchInstanceMetadataService.ts12
-rw-r--r--packages/backend/src/core/HttpRequestService.ts11
-rw-r--r--packages/backend/src/core/MfmService.ts175
-rw-r--r--packages/backend/src/core/NoteCreateService.ts81
-rw-r--r--packages/backend/src/core/NoteDeleteService.ts17
-rw-r--r--packages/backend/src/core/NoteEditService.ts46
-rw-r--r--packages/backend/src/core/PollService.ts2
-rw-r--r--packages/backend/src/core/S3Service.ts2
-rw-r--r--packages/backend/src/core/SearchService.ts395
-rw-r--r--packages/backend/src/core/SignupService.ts1
-rw-r--r--packages/backend/src/core/SystemWebhookService.ts31
-rw-r--r--packages/backend/src/core/UserBlockingService.ts8
-rw-r--r--packages/backend/src/core/UserFollowingService.ts32
-rw-r--r--packages/backend/src/core/UserListService.ts2
-rw-r--r--packages/backend/src/core/UserService.ts9
-rw-r--r--packages/backend/src/core/UserWebhookService.ts25
-rw-r--r--packages/backend/src/core/UtilityService.ts7
-rw-r--r--packages/backend/src/core/WebAuthnService.ts4
-rw-r--r--packages/backend/src/core/WebhookTestService.ts4
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts8
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts34
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts32
-rw-r--r--packages/backend/src/core/activitypub/ApResolverService.ts108
-rw-r--r--packages/backend/src/core/activitypub/ApUtilityService.ts108
-rw-r--r--packages/backend/src/core/activitypub/misc/check-against-url.ts31
-rw-r--r--packages/backend/src/core/activitypub/misc/contexts.ts5
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts223
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts35
-rw-r--r--packages/backend/src/core/activitypub/type.ts13
-rw-r--r--packages/backend/src/core/chart/ChartManagementService.ts2
-rw-r--r--packages/backend/src/core/entities/EmojiEntityService.ts90
-rw-r--r--packages/backend/src/core/entities/InstanceEntityService.ts1
-rw-r--r--packages/backend/src/core/entities/MetaEntityService.ts3
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts83
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts58
-rw-r--r--packages/backend/src/daemons/ApLogCleanupService.ts62
-rw-r--r--packages/backend/src/daemons/DaemonModule.ts3
-rw-r--r--packages/backend/src/di-symbols.ts4
-rw-r--r--packages/backend/src/logger.ts4
-rw-r--r--packages/backend/src/misc/append-content-warning.ts62
-rw-r--r--packages/backend/src/misc/gen-identicon.ts4
-rw-r--r--packages/backend/src/misc/get-note-summary.ts11
-rw-r--r--packages/backend/src/misc/identifiable-error.ts8
-rw-r--r--packages/backend/src/misc/is-retryable-error.ts22
-rw-r--r--packages/backend/src/misc/json-schema.ts7
-rw-r--r--packages/backend/src/models/Instance.ts9
-rw-r--r--packages/backend/src/models/Meta.ts5
-rw-r--r--packages/backend/src/models/Note.ts12
-rw-r--r--packages/backend/src/models/Page.ts2
-rw-r--r--packages/backend/src/models/RepositoryModule.ts29
-rw-r--r--packages/backend/src/models/SkApContext.ts25
-rw-r--r--packages/backend/src/models/SkApFetchLog.ts89
-rw-r--r--packages/backend/src/models/SkApInboxLog.ts109
-rw-r--r--packages/backend/src/models/User.ts18
-rw-r--r--packages/backend/src/models/UserProfile.ts19
-rw-r--r--packages/backend/src/models/_.ts9
-rw-r--r--packages/backend/src/models/json-schema/emoji.ts83
-rw-r--r--packages/backend/src/models/json-schema/federation-instance.ts5
-rw-r--r--packages/backend/src/models/json-schema/meta.ts9
-rw-r--r--packages/backend/src/models/json-schema/note.ts13
-rw-r--r--packages/backend/src/models/json-schema/user.ts17
-rw-r--r--packages/backend/src/postgres.ts65
-rw-r--r--packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts26
-rw-r--r--packages/backend/src/queue/processors/CleanChartsProcessorService.ts1
-rw-r--r--packages/backend/src/queue/processors/DeleteAccountProcessorService.ts41
-rw-r--r--packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts11
-rw-r--r--packages/backend/src/queue/processors/ImportNotesProcessorService.ts4
-rw-r--r--packages/backend/src/queue/processors/InboxProcessorService.ts57
-rw-r--r--packages/backend/src/queue/processors/ResyncChartsProcessorService.ts1
-rw-r--r--packages/backend/src/queue/processors/TickChartsProcessorService.ts1
-rw-r--r--packages/backend/src/queue/types.ts17
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts25
-rw-r--r--packages/backend/src/server/FileServerService.ts23
-rw-r--r--packages/backend/src/server/ServerModule.ts6
-rw-r--r--packages/backend/src/server/ServerService.ts2
-rw-r--r--packages/backend/src/server/SkRateLimiterService.md6
-rw-r--r--packages/backend/src/server/SkRateLimiterService.ts (renamed from packages/backend/src/server/api/SkRateLimiterService.ts)55
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts39
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts1614
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts13
-rw-r--r--packages/backend/src/server/api/SigninWithPasskeyApiService.ts2
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts3
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts18
-rw-r--r--packages/backend/src/server/api/endpoint-list.ts421
-rw-r--r--packages/backend/src/server/api/endpoints.ts818
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/captcha/current.ts77
-rw-r--r--packages/backend/src/server/api/endpoints/admin/captcha/save.ts129
-rw-r--r--packages/backend/src/server/api/endpoints/admin/cw-user.ts67
-rw-r--r--packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/copy.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts14
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/admin/invite/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/nsfw-user.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/admin/promo/create.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/admin/reject-quotes.ts63
-rw-r--r--packages/backend/src/server/api/endpoints/admin/relays/add.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/relays/remove.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/silence-user.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts21
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unsilence-user.ts22
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/ap/show.ts92
-rw-r--r--packages/backend/src/server/api/endpoints/channels/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/delete.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/update.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/update.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/emojis.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/federation/update-remote-user.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/flash/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/i.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/password-less.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/unregister.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/update-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/apps.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/change-password.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/claim-achievement.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/delete-account.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/regenerate-token.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/mute/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/notes/edit.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/notes/favorites/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/like.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/create.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/list.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/pages/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/pages/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/pages/update.ts25
-rw-r--r--packages/backend/src/server/api/endpoints/server-info.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/users/relation.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts126
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonApiServerService.ts850
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonDataService.ts84
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonLogger.ts39
-rw-r--r--packages/backend/src/server/api/mastodon/converters.ts241
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/account.ts294
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/auth.ts76
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/filter.ts93
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/meta.ts8
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/notifications.ts85
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/search.ts138
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/status.ts528
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/timeline.ts307
-rw-r--r--packages/backend/src/server/api/mastodon/timelineArgs.ts47
-rw-r--r--packages/backend/src/server/api/openapi/schemas.ts4
-rw-r--r--packages/backend/src/server/api/stream/Connection.ts8
-rw-r--r--packages/backend/src/server/api/stream/channel.ts61
-rw-r--r--packages/backend/src/server/api/stream/channels/admin.ts3
-rw-r--r--packages/backend/src/server/api/stream/channels/antenna.ts4
-rw-r--r--packages/backend/src/server/api/stream/channels/bubble-timeline.ts20
-rw-r--r--packages/backend/src/server/api/stream/channels/channel.ts17
-rw-r--r--packages/backend/src/server/api/stream/channels/drive.ts3
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts16
-rw-r--r--packages/backend/src/server/api/stream/channels/hashtag.ts17
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts16
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts17
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts16
-rw-r--r--packages/backend/src/server/api/stream/channels/main.ts4
-rw-r--r--packages/backend/src/server/api/stream/channels/queue-stats.ts7
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi-game.ts6
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi.ts6
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts11
-rw-r--r--packages/backend/src/server/api/stream/channels/server-stats.ts7
-rw-r--r--packages/backend/src/server/api/stream/channels/user-list.ts16
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts13
-rw-r--r--packages/backend/src/server/web/boot.embed.js2
-rw-r--r--packages/backend/src/server/web/boot.js2
-rw-r--r--packages/backend/src/types.ts115
-rw-r--r--packages/backend/test-federation/compose.tpl.yml1
-rw-r--r--packages/backend/test-federation/compose.yml3
-rw-r--r--packages/backend/test-federation/test/note.test.ts6
-rw-r--r--packages/backend/test/e2e/timelines.ts2
-rw-r--r--packages/backend/test/misc/mock-resolver.ts4
-rw-r--r--packages/backend/test/unit/AbuseReportNotificationService.ts50
-rw-r--r--packages/backend/test/unit/CaptchaService.ts639
-rw-r--r--packages/backend/test/unit/CustomEmojiService.ts823
-rw-r--r--packages/backend/test/unit/MfmService.ts69
-rw-r--r--packages/backend/test/unit/NoteCreateService.ts1
-rw-r--r--packages/backend/test/unit/SigninWithPasskeyApiService.ts2
-rw-r--r--packages/backend/test/unit/SystemWebhookService.ts49
-rw-r--r--packages/backend/test/unit/UserWebhookService.ts91
-rw-r--r--packages/backend/test/unit/activitypub.ts214
-rw-r--r--packages/backend/test/unit/core/activitypub/ApUtilityService.ts354
-rw-r--r--packages/backend/test/unit/misc/append-content-warning.ts92
-rw-r--r--packages/backend/test/unit/misc/check-against-url.ts55
-rw-r--r--packages/backend/test/unit/misc/is-renote.ts1
-rw-r--r--packages/backend/test/unit/misc/is-retryable-error.ts73
-rw-r--r--packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts12
-rw-r--r--packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts68
-rw-r--r--packages/frontend-embed/package.json5
-rw-r--r--packages/frontend-embed/src/boot.ts20
-rw-r--r--packages/frontend-embed/src/components/EmAcct.vue2
-rw-r--r--packages/frontend-embed/src/components/EmMention.vue2
-rw-r--r--packages/frontend-embed/src/components/EmNote.vue9
-rw-r--r--packages/frontend-embed/src/components/EmNoteDetailed.vue9
-rw-r--r--packages/frontend-embed/src/components/EmNoteSimple.vue11
-rw-r--r--packages/frontend-embed/src/components/EmNoteSub.vue11
-rw-r--r--packages/frontend-embed/src/components/EmNotes.vue3
-rw-r--r--packages/frontend-embed/src/components/EmUrl.vue2
-rw-r--r--packages/frontend-embed/src/index.html38
-rw-r--r--packages/frontend-embed/src/theme.ts23
-rw-r--r--packages/frontend-embed/tsconfig.json4
-rw-r--r--packages/frontend-embed/vite.config.local-dev.ts96
-rw-r--r--packages/frontend-embed/vite.config.ts7
-rw-r--r--packages/frontend-shared/build.js8
-rw-r--r--packages/frontend-shared/js/append-content-warning.ts62
-rw-r--r--packages/frontend-shared/js/compute-merged-cw.ts17
-rw-r--r--packages/frontend-shared/package.json2
-rw-r--r--packages/frontend/.storybook/fake-utils.ts154
-rw-r--r--packages/frontend/.storybook/fakes.ts91
-rw-r--r--packages/frontend/.storybook/generate.tsx4
-rw-r--r--packages/frontend/eslint.config.js1
-rw-r--r--packages/frontend/package.json7
-rw-r--r--packages/frontend/src/_dev_boot_.ts90
-rw-r--r--packages/frontend/src/account.ts8
-rw-r--r--packages/frontend/src/boot/common.ts5
-rw-r--r--packages/frontend/src/boot/main-boot.ts40
-rw-r--r--packages/frontend/src/components/DynamicNote.vue49
-rw-r--r--packages/frontend/src/components/MkAsUi.vue19
-rw-r--r--packages/frontend/src/components/MkCaptcha.vue64
-rw-r--r--packages/frontend/src/components/MkChannelPreview.vue2
-rw-r--r--packages/frontend/src/components/MkContainer.vue10
-rw-r--r--packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue2
-rw-r--r--packages/frontend/src/components/MkDriveFileThumbnail.vue28
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.vue4
-rw-r--r--packages/frontend/src/components/MkFolder.vue6
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue12
-rw-r--r--packages/frontend/src/components/MkFormFooter.vue9
-rw-r--r--packages/frontend/src/components/MkImgWithBlurhash.vue2
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue18
-rw-r--r--packages/frontend/src/components/MkInstanceTicker.vue41
-rw-r--r--packages/frontend/src/components/MkMention.vue2
-rw-r--r--packages/frontend/src/components/MkModal.vue29
-rw-r--r--packages/frontend/src/components/MkNote.vue58
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue30
-rw-r--r--packages/frontend/src/components/MkNoteMediaGrid.vue109
-rw-r--r--packages/frontend/src/components/MkNoteSimple.vue11
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue22
-rw-r--r--packages/frontend/src/components/MkNotes.vue2
-rw-r--r--packages/frontend/src/components/MkNotification.vue45
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue2
-rw-r--r--packages/frontend/src/components/MkPagingButtons.vue124
-rw-r--r--packages/frontend/src/components/MkPoll.vue4
-rw-r--r--packages/frontend/src/components/MkPostForm.vue46
-rw-r--r--packages/frontend/src/components/MkPostFormAttaches.vue8
-rw-r--r--packages/frontend/src/components/MkRemoteEmojiEditDialog.vue132
-rw-r--r--packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts106
-rw-r--r--packages/frontend/src/components/MkRoleSelectDialog.vue200
-rw-r--r--packages/frontend/src/components/MkSignin.input.vue2
-rw-r--r--packages/frontend/src/components/MkSignin.vue1
-rw-r--r--packages/frontend/src/components/MkSignupDialog.form.vue4
-rw-r--r--packages/frontend/src/components/MkSignupDialog.rules.vue6
-rw-r--r--packages/frontend/src/components/MkSortOrderEditor.define.ts11
-rw-r--r--packages/frontend/src/components/MkSortOrderEditor.vue118
-rw-r--r--packages/frontend/src/components/MkSparkle.vue53
-rw-r--r--packages/frontend/src/components/MkSubNoteContent.vue4
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue2
-rw-r--r--packages/frontend/src/components/MkTagItem.stories.impl.ts70
-rw-r--r--packages/frontend/src/components/MkTagItem.vue76
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue29
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue16
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.vue6
-rw-r--r--packages/frontend/src/components/MkWidgets.vue19
-rw-r--r--packages/frontend/src/components/SkErrorList.vue43
-rw-r--r--packages/frontend/src/components/SkFetchNote.vue69
-rw-r--r--packages/frontend/src/components/SkInstanceTicker.vue48
-rw-r--r--packages/frontend/src/components/SkMfmWindow.vue13
-rw-r--r--packages/frontend/src/components/SkNote.vue56
-rw-r--r--packages/frontend/src/components/SkNoteDetailed.vue33
-rw-r--r--packages/frontend/src/components/SkNoteHeader.vue7
-rw-r--r--packages/frontend/src/components/SkNoteSimple.vue11
-rw-r--r--packages/frontend/src/components/SkNoteSub.vue22
-rw-r--r--packages/frontend/src/components/global/MkAcct.vue2
-rw-r--r--packages/frontend/src/components/global/MkLazy.vue10
-rw-r--r--packages/frontend/src/components/global/MkMfm.ts8
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue12
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue2
-rw-r--r--packages/frontend/src/components/grid/MkCellTooltip.vue35
-rw-r--r--packages/frontend/src/components/grid/MkDataCell.vue418
-rw-r--r--packages/frontend/src/components/grid/MkDataRow.vue72
-rw-r--r--packages/frontend/src/components/grid/MkGrid.stories.impl.ts223
-rw-r--r--packages/frontend/src/components/grid/MkGrid.vue1374
-rw-r--r--packages/frontend/src/components/grid/MkHeaderCell.vue216
-rw-r--r--packages/frontend/src/components/grid/MkHeaderRow.vue60
-rw-r--r--packages/frontend/src/components/grid/MkNumberCell.vue61
-rw-r--r--packages/frontend/src/components/grid/cell-validators.ts110
-rw-r--r--packages/frontend/src/components/grid/cell.ts88
-rw-r--r--packages/frontend/src/components/grid/column.ts53
-rw-r--r--packages/frontend/src/components/grid/grid-event.ts46
-rw-r--r--packages/frontend/src/components/grid/grid-utils.ts215
-rw-r--r--packages/frontend/src/components/grid/grid.ts49
-rw-r--r--packages/frontend/src/components/grid/row.ts68
-rw-r--r--packages/frontend/src/components/hook/useLoading.ts52
-rw-r--r--packages/frontend/src/components/page/page.text.vue2
-rw-r--r--packages/frontend/src/index.html39
-rw-r--r--packages/frontend/src/os.ts27
-rw-r--r--packages/frontend/src/pages/about.vue47
-rw-r--r--packages/frontend/src/pages/admin-user.vue44
-rw-r--r--packages/frontend/src/pages/admin/bot-protection.vue278
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts57
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue39
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue213
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue660
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue481
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.vue35
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue88
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue503
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts160
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager2.vue51
-rw-r--r--packages/frontend/src/pages/admin/federation.vue1
-rw-r--r--packages/frontend/src/pages/admin/index.vue16
-rw-r--r--packages/frontend/src/pages/admin/modlog.ModLog.vue92
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue10
-rw-r--r--packages/frontend/src/pages/admin/roles.vue13
-rw-r--r--packages/frontend/src/pages/admin/settings.vue21
-rw-r--r--packages/frontend/src/pages/admin/users.vue4
-rw-r--r--packages/frontend/src/pages/api-console.vue2
-rw-r--r--packages/frontend/src/pages/channel.vue6
-rw-r--r--packages/frontend/src/pages/channels.vue35
-rw-r--r--packages/frontend/src/pages/clip.vue5
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue29
-rw-r--r--packages/frontend/src/pages/drive.file.info.vue2
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue2
-rw-r--r--packages/frontend/src/pages/explore.users.vue3
-rw-r--r--packages/frontend/src/pages/instance-info.vue12
-rw-r--r--packages/frontend/src/pages/miauth.vue21
-rw-r--r--packages/frontend/src/pages/note.vue20
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.vue111
-rw-r--r--packages/frontend/src/pages/page.vue6
-rw-r--r--packages/frontend/src/pages/scratchpad.vue2
-rw-r--r--packages/frontend/src/pages/search.note.vue40
-rw-r--r--packages/frontend/src/pages/search.user.vue44
-rw-r--r--packages/frontend/src/pages/settings/accounts.vue107
-rw-r--r--packages/frontend/src/pages/settings/email.vue44
-rw-r--r--packages/frontend/src/pages/settings/general.vue23
-rw-r--r--packages/frontend/src/pages/settings/index.vue2
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue29
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue31
-rw-r--r--packages/frontend/src/pages/settings/profile.vue2
-rw-r--r--packages/frontend/src/pages/settings/statusbar.statusbar.vue3
-rw-r--r--packages/frontend/src/pages/theme-editor.vue2
-rw-r--r--packages/frontend/src/pages/user/files.vue56
-rw-r--r--packages/frontend/src/pages/user/home.vue108
-rw-r--r--packages/frontend/src/pages/user/index.files.vue58
-rw-r--r--packages/frontend/src/pages/user/index.vue13
-rw-r--r--packages/frontend/src/pages/welcome.entrance.a.vue2
-rw-r--r--packages/frontend/src/plugin.ts2
-rw-r--r--packages/frontend/src/router/definition.ts9
-rw-r--r--packages/frontend/src/scripts/aiscript/api.ts46
-rw-r--r--packages/frontend/src/scripts/aiscript/common.ts15
-rw-r--r--packages/frontend/src/scripts/aiscript/ui.ts139
-rw-r--r--packages/frontend/src/scripts/autocomplete.ts2
-rw-r--r--packages/frontend/src/scripts/boost-quote.ts10
-rw-r--r--packages/frontend/src/scripts/check-word-mute.ts6
-rw-r--r--packages/frontend/src/scripts/code-highlighter.ts6
-rw-r--r--packages/frontend/src/scripts/file-drop.ts121
-rw-r--r--packages/frontend/src/scripts/get-note-menu.ts27
-rw-r--r--packages/frontend/src/scripts/get-note-summary.ts11
-rw-r--r--packages/frontend/src/scripts/get-user-menu.ts2
-rw-r--r--packages/frontend/src/scripts/key-event.ts153
-rw-r--r--packages/frontend/src/scripts/lookup.ts38
-rw-r--r--packages/frontend/src/scripts/merge.ts8
-rw-r--r--packages/frontend/src/scripts/misskey-api.ts18
-rw-r--r--packages/frontend/src/scripts/please-login.ts12
-rw-r--r--packages/frontend/src/scripts/sanitize-html.ts1
-rw-r--r--packages/frontend/src/scripts/select-file.ts20
-rw-r--r--packages/frontend/src/scripts/sound.ts4
-rw-r--r--packages/frontend/src/scripts/use-note-capture.ts1
-rw-r--r--packages/frontend/src/scripts/warning-external-website.ts20
-rw-r--r--packages/frontend/src/server-context.ts12
-rw-r--r--packages/frontend/src/store.ts6
-rw-r--r--packages/frontend/src/ui/_common_/common.ts20
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue22
-rw-r--r--packages/frontend/src/ui/_common_/statusbars.vue3
-rw-r--r--packages/frontend/src/ui/classic.vue1
-rw-r--r--packages/frontend/src/ui/deck.vue26
-rw-r--r--packages/frontend/src/ui/deck/deck-store.ts8
-rw-r--r--packages/frontend/src/ui/universal.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetRss.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetRssTicker.vue2
-rw-r--r--packages/frontend/src/widgets/index.ts10
-rw-r--r--packages/frontend/test/aiscript/api.test.ts401
-rw-r--r--packages/frontend/test/aiscript/common.test.ts23
-rw-r--r--packages/frontend/test/aiscript/ui.test.ts826
-rw-r--r--packages/frontend/tsconfig.json4
-rw-r--r--packages/frontend/vite.config.local-dev.ts102
-rw-r--r--packages/frontend/vite.config.ts7
-rw-r--r--packages/frontend/vite.replaceIcons.ts7
-rw-r--r--packages/megalodon/package.json6
-rw-r--r--packages/megalodon/src/entities/relationship.ts1
-rw-r--r--packages/megalodon/src/index.ts5
-rw-r--r--packages/megalodon/src/mastodon/entities/relationship.ts1
-rw-r--r--packages/megalodon/src/misskey.ts4
-rw-r--r--packages/megalodon/src/misskey/api_client.ts11
-rw-r--r--packages/megalodon/src/misskey/entities/relation.ts3
-rw-r--r--packages/megalodon/test/integration/detector.spec.ts68
-rw-r--r--packages/megalodon/tsconfig.json4
-rw-r--r--packages/misskey-bubble-game/build.js8
-rw-r--r--packages/misskey-js/LICENSE2
-rw-r--r--packages/misskey-js/api-extractor.json2
-rw-r--r--packages/misskey-js/build.js9
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md393
-rw-r--r--packages/misskey-js/generator/package.json1
-rw-r--r--packages/misskey-js/package.json2
-rw-r--r--packages/misskey-js/src/api.ts2
-rw-r--r--packages/misskey-js/src/autogen/apiClientJSDoc.ts1146
-rw-r--r--packages/misskey-js/src/autogen/endpoint.ts506
-rw-r--r--packages/misskey-js/src/autogen/entities.ts288
-rw-r--r--packages/misskey-js/src/autogen/models.ts1
-rw-r--r--packages/misskey-js/src/autogen/types.ts8695
-rw-r--r--packages/misskey-js/src/consts.ts131
-rw-r--r--packages/misskey-js/src/entities.ts72
-rw-r--r--packages/misskey-js/src/streaming.ts16
-rw-r--r--packages/misskey-reversi/build.js8
-rw-r--r--packages/shared/eslint.config.js7
-rw-r--r--pnpm-lock.yaml335
-rw-r--r--scripts/dev.mjs14
-rw-r--r--sharkey-locales/en-US.yml81
521 files changed, 28055 insertions, 11819 deletions
diff --git a/.config/ci.yml b/.config/ci.yml
index 8730ccab3a..def276ca58 100644
--- a/.config/ci.yml
+++ b/.config/ci.yml
@@ -103,10 +103,38 @@ redis:
# #prefix: example-prefix
# #db: 1
-# ┌───────────────────────────┐
-#───┘ MeiliSearch configuration └─────────────────────────────
+#redisForRateLimit:
+# host: localhost
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+# # You can specify more ioredis options...
+# #username: example-username
+
+# ┌───────────────────────────────┐
+#───┘ Fulltext search configuration └─────────────────────────────
-# You can set scope to local (default value) or global
+# These are the setting items for the full-text search provider.
+fulltextSearch:
+ # You can select the ID generation method.
+ # - sqlLike (default)
+ # Use SQL-like search.
+ # This is a standard feature of PostgreSQL, so no special extensions are required.
+ # - sqlPgroonga
+ # Use pgroonga.
+ # You need to install pgroonga and configure it as a PostgreSQL extension.
+ # In addition to the above, you need to create a pgroonga index on the text column of the note table.
+ # see: https://pgroonga.github.io/tutorial/
+ # - meilisearch
+ # Use Meilisearch.
+ # You need to install Meilisearch and configure.
+ provider: sqlLike
+
+# For Meilisearch settings.
+# If you select "meilisearch" for "fulltextSearch.provider", it must be set.
+# You can set scope to local or global (default value)
# (include notes from remote).
#meilisearch:
@@ -234,3 +262,28 @@ checkActivityPubGetSignature: false
# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
#filePermissionBits: '644'
+
+# Log settings
+# logging:
+# sql:
+# # Outputs query parameters during SQL execution to the log.
+# # default: false
+# enableQueryParamLogging: false
+# # Disable query truncation. If set to true, the full text of the query will be output to the log.
+# # default: false
+# disableQueryTruncation: false
+
+# Settings for the activity logger, which records inbound activities to the database.
+# Disabled by default due to the large volume of data it saves.
+#activityLogging:
+ # Log activities to the database (default: false)
+ #enabled: false
+
+ # Save the activity before processing, then update later with the results.
+ # This has the advantage of capturing activities that cause a hard-crash, but doubles the number of queries used.
+ # Default: false
+ #preSave: false
+
+ # How long to save each log entry before deleting it.
+ # Default: 2592000000 (1 week)
+ #maxAge: 2592000000
diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml
index c8c4a36d8f..9e4961c325 100644
--- a/.config/cypress-devcontainer.yml
+++ b/.config/cypress-devcontainer.yml
@@ -124,8 +124,39 @@ redis:
# #prefix: example-prefix
# #db: 1
-# ┌───────────────────────────┐
-#───┘ MeiliSearch configuration └─────────────────────────────
+#redisForRateLimit:
+# host: localhost
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+# # You can specify more ioredis options...
+# #username: example-username
+
+# ┌───────────────────────────────┐
+#───┘ Fulltext search configuration └─────────────────────────────
+
+# These are the setting items for the full-text search provider.
+fulltextSearch:
+ # You can select the ID generation method.
+ # - sqlLike (default)
+ # Use SQL-like search.
+ # This is a standard feature of PostgreSQL, so no special extensions are required.
+ # - sqlPgroonga
+ # Use pgroonga.
+ # You need to install pgroonga and configure it as a PostgreSQL extension.
+ # In addition to the above, you need to create a pgroonga index on the text column of the note table.
+ # see: https://pgroonga.github.io/tutorial/
+ # - meilisearch
+ # Use Meilisearch.
+ # You need to install Meilisearch and configure.
+ provider: sqlLike
+
+# For Meilisearch settings.
+# If you select "meilisearch" for "fulltextSearch.provider", it must be set.
+# You can set scope to local or global (default value)
+# (include notes from remote).
#meilisearch:
# host: meilisearch
@@ -133,6 +164,7 @@ redis:
# apiKey: ''
# ssl: true
# index: ''
+# scope: global
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────
@@ -240,3 +272,28 @@ allowedPrivateNetworks: [
# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
#filePermissionBits: '644'
+
+# Log settings
+# logging:
+# sql:
+# # Outputs query parameters during SQL execution to the log.
+# # default: false
+# enableQueryParamLogging: false
+# # Disable query truncation. If set to true, the full text of the query will be output to the log.
+# # default: false
+# disableQueryTruncation: false
+
+# Settings for the activity logger, which records inbound activities to the database.
+# Disabled by default due to the large volume of data it saves.
+#activityLogging:
+ # Log activities to the database (default: false)
+ #enabled: false
+
+ # Save the activity before processing, then update later with the results.
+ # This has the advantage of capturing activities that cause a hard-crash, but doubles the number of queries used.
+ # Default: false
+ #preSave: false
+
+ # How long to save each log entry before deleting it.
+ # Default: 2592000000 (1 week)
+ #maxAge: 2592000000
diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index ce2daf3aec..f798fd8246 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -38,14 +38,14 @@
# Option 3: If neither of the above applies to you.
# (In this case, the source code should be published
# on the Misskey interface. IT IS NOT ENOUGH TO
-# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY
+# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY
# E-MAIL OR OTHER MEANS. If you are not satisfied
# with this, it is recommended that you read the
# license again carefully. Anyway, enabling this
# option will automatically generate and publish a
# tarball at build time, protecting you from
# inadvertent license violations. (There is no legal
-# guarantee, of course.) The tarball will generated
+# guarantee, of course.) The tarball will be generated
# from the root directory of your codebase. So it is
# also recommended to check <built/tarball> directory
# once after building and before activating the server
@@ -171,10 +171,38 @@ redis:
# #prefix: example-prefix
# #db: 1
-# ┌───────────────────────────┐
-#───┘ MeiliSearch configuration └─────────────────────────────
+#redisForRateLimit:
+# host: localhost
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+# # You can specify more ioredis options...
+# #username: example-username
+
+# ┌───────────────────────────────┐
+#───┘ Fulltext search configuration └─────────────────────────────
-# You can set scope to local (default value) or global
+# These are the setting items for the full-text search provider.
+fulltextSearch:
+ # You can select the ID generation method.
+ # - sqlLike (default)
+ # Use SQL-like search.
+ # This is a standard feature of PostgreSQL, so no special extensions are required.
+ # - sqlPgroonga
+ # Use pgroonga.
+ # You need to install pgroonga and configure it as a PostgreSQL extension.
+ # In addition to the above, you need to create a pgroonga index on the text column of the note table.
+ # see: https://pgroonga.github.io/tutorial/
+ # - meilisearch
+ # Use Meilisearch.
+ # You need to install Meilisearch and configure.
+ provider: sqlLike
+
+# For Meilisearch settings.
+# If you select "meilisearch" for "fulltextSearch.provider", it must be set.
+# You can set scope to local or global (default value)
# (include notes from remote).
#meilisearch:
@@ -317,3 +345,28 @@ checkActivityPubGetSignature: false
# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
#filePermissionBits: '644'
+
+# Log settings
+# logging:
+# sql:
+# # Outputs query parameters during SQL execution to the log.
+# # default: false
+# enableQueryParamLogging: false
+# # Disable query truncation. If set to true, the full text of the query will be output to the log.
+# # default: false
+# disableQueryTruncation: false
+
+# Settings for the activity logger, which records inbound activities to the database.
+# Disabled by default due to the large volume of data it saves.
+#activityLogging:
+ # Log activities to the database (default: false)
+ #enabled: false
+
+ # Save the activity before processing, then update later with the results.
+ # This has the advantage of capturing activities that cause a hard-crash, but doubles the number of queries used.
+ # Default: false
+ #preSave: false
+
+ # How long to save each log entry before deleting it.
+ # Default: 2592000000 (1 week)
+ #maxAge: 2592000000
diff --git a/.config/example.yml b/.config/example.yml
index ba8e818b5d..d199544589 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -100,6 +100,8 @@ url: https://example.tld/
# The port that your Misskey server should listen on.
port: 3000
+# the address to bind to, defaults to "every address"
+# address: '0.0.0.0'
# You can also use UNIX domain socket.
# socket: /path/to/misskey.sock
@@ -196,10 +198,51 @@ redis:
# # You can specify more ioredis options...
# #username: example-username
-# ┌───────────────────────────┐
-#───┘ MeiliSearch configuration └─────────────────────────────
+#redisForRateLimit:
+# host: localhost
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+# # You can specify more ioredis options...
+# #username: example-username
+
+# ┌───────────────────────────────┐
+#───┘ Fulltext search configuration └─────────────────────────────
-# You can set scope to local (default value) or global
+# These are the setting items for the full-text search provider.
+fulltextSearch:
+ # You can select the ID generation method.
+ # - sqlLike (default)
+ # Use SQL-like search.
+ # This is a standard feature of PostgreSQL, so no special extensions are required.
+ # - sqlPgroonga
+ # Use pgroonga.
+ # You need to install pgroonga and configure it as a PostgreSQL extension.
+ # In addition to the above, you need to create a pgroonga index on the text column of the note table.
+ # see: https://pgroonga.github.io/tutorial/
+ # - sqlTsvector
+ # Use Postgres tsvectors.
+ # You need to create a generated column and index on the note table to use this, followed by an ANALYZE on the table. Beware, this will take a while to be created and the database will remain locked during this process.
+ # This also enables advanced search syntax, see documentation of websearch_to_tsquery: https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
+ # Support for multiple languages is currently rather poor and will be improved once post languages become a feature.
+ #
+ # Example to set up tsvectors for an English instance:
+ # ALTER TABLE note ADD COLUMN tsvector_embedding tsvector GENERATED ALWAYS AS ( to_tsvector('english', COALESCE(text, '') || ' ' || COALESCE(cw, '') || ' ' || COALESCE(name, ''))) STORED;
+ # CREATE INDEX vector_idx ON note USING GIN (tsvector_embedding);
+ # ANALYZE note;
+ #
+ # Note: You can opt to use a different dictionary for better results if your main instance language is not English.
+ # To get a list, use "SELECT cfgname FROM pg_ts_config;" and replace 'english' with the desired dictionary name.
+ # - meilisearch
+ # Use Meilisearch.
+ # You need to install Meilisearch and configure.
+ provider: sqlLike
+
+# For Meilisearch settings.
+# If you select "meilisearch" for "fulltextSearch.provider", it must be set.
+# You can set scope to local or global (default value)
# (include notes from remote).
#meilisearch:
@@ -341,9 +384,9 @@ checkActivityPubGetSignature: false
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
-# timeout and maximum size for imports (e.g. note imports)
+# timeout (in milliseconds) and maximum size for imports (e.g. note imports)
#import:
-# downloadTimeout: 30
+# downloadTimeout: 30000
# maxFileSize: 262144000
# PID File of master process
@@ -353,3 +396,28 @@ checkActivityPubGetSignature: false
# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
#filePermissionBits: '644'
+
+# Log settings
+# logging:
+# sql:
+# # Outputs query parameters during SQL execution to the log.
+# # default: false
+# enableQueryParamLogging: false
+# # Disable query truncation. If set to true, the full text of the query will be output to the log.
+# # default: false
+# disableQueryTruncation: false
+
+# Settings for the activity logger, which records inbound activities to the database.
+# Disabled by default due to the large volume of data it saves.
+#activityLogging:
+ # Log activities to the database (default: false)
+ #enabled: false
+
+ # Save the activity before processing, then update later with the results.
+ # This has the advantage of capturing activities that cause a hard-crash, but doubles the number of queries used.
+ # Default: false
+ #preSave: false
+
+ # How long to save each log entry before deleting it.
+ # Default: 2592000000 (1 week)
+ #maxAge: 2592000000
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4b80ba9728..256cb08fe9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,7 +4,7 @@ stages:
testCommit:
stage: test
- image: node:iron
+ image: node:jod
services:
- postgres:15
- redis
@@ -21,8 +21,8 @@ testCommit:
- pnpm install --frozen-lockfile
- pnpm run build
- pnpm run migrate
- - pnpm run --filter='!megalodon' test
- - pnpm run --filter=backend --filter=misskey-js lint
+ - pnpm run test
+ - pnpm run --filter=backend --filter=misskey-js --filter=frontend-shared lint
- pnpm run --filter=frontend --filter=frontend-embed eslint
cache:
key: test
diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md
index e6977def70..389b2c8cbe 100644
--- a/.gitlab/merge_request_templates/default.md
+++ b/.gitlab/merge_request_templates/default.md
@@ -3,10 +3,12 @@
# **What does this MR do?**
<!-- Please give us a brief description of what this PR does. -->
+%{all_commits}
+
# **Contribution Guidelines**
By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md)
- [ ] I agree to follow this project's Contribution Guidelines
- [ ] I have made sure to test this merge request
<!-- Uncomment if your merge request has multiple authors -->
-<!-- Co-authored-by: Name <email@email.com> -->
+<!-- %{co_authored_by} -->
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2dbc457710..5d36875eb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,103 @@
+## 2025.2.0
+
+### General
+- Fix: Docker のビルドに失敗する問題を修正
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/883)
+
+### Client
+- Fix: パスキーでパスワードレスログインが出来ない問題を修正
+- Fix: 一部環境でセンシティブなファイルを含むノートの非表示が効かない問題
+- Fix: データセーバー有効時にもユーザーページの「ファイル」タブで画像が読み込まれてしまう問題を修正
+- Fix: MFMの `sparkle` エフェクトが正しく表示されない問題を修正
+- Fix: ページのURLにスラッシュが含まれている場合にページが正しく表示されない問題を修正
+- Fix: デッキのプロファイルが新規作成できない問題を修正
+- Fix: セキュリティに関する修正
+- ローカライゼーションの更新
+- Playが実装されたため、ページ機能の「ソースを見る」は削除されました
+
+### Server
+- Enhance: ページのURLに使用可能な文字を限定するように
+- Fix: 個別お知らせページのmetaタグ出力の条件が間違っていたのを修正
+
+## 2025.1.0
+
+### Note
+- [重要] ノート検索プロバイダの追加に伴い、configファイル(default.ymlなど)の構成が少し変わります.
+ - 新しい設定項目"fulltextSearch.provider"が追加されました. sqlLike, sqlPgroonga, meilisearchのいずれかを設定出来ます.
+ - すでにMeilisearchをお使いの場合、 **"fulltextSearch.provider"を"meilisearch"に設定する必要** があります.
+ - 詳細は #14730 および `.config/example.yml` または `.config/docker_example.yml`の'Fulltext search configuration'をご参照願います.
+- 【開発者向け】従来の開発モードでHMRが機能しない問題が修正されたため、バックエンド・フロントエンド分離型の開発モードが削除されました。開発環境においてconfigの変更が必要となる可能性があります。
+
+### General
+- Feat: カスタム絵文字管理画面をリニューアル #10996
+ * β版として公開のため、旧画面も引き続き利用可能です
+
+### Client
+- Enhance: PC画面でチャンネルが複数列で表示されるように
+ (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
+- Enhance: 照会に失敗した場合、その理由を表示するように
+- Enhance: ワードミュートで検知されたワードを表示できるように
+- Enhance: リモートのノートのリンクをコピーできるように
+- Enhance: 連合がホワイトリスト化・無効化されているサーバー向けのデザイン修正
+- Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加
+- Enhance: ノートの添付ファイルを一覧で遡れる「ファイル」タブを追加
+ (Based on https://github.com/Otaku-Social/maniakey/pull/14)
+- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
+- Enhance: クエリパラメータでuiを一時的に変更できるように #15240
+- Enhance: リモート絵文字のインポート時に詳細を確認できるように #15336
+- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
+- Fix: サーバー情報メニューに区切り線が不足していたのを修正
+- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
+- Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803)
+- Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正
+- Fix: プラグイン `register_note_view_interruptor` でノートのサーバー情報の書き換えができない問題を修正
+- Fix: Botプロテクションの設定変更時は実際に検証を通過しないと保存できないように( #15137 )
+- Fix: ノート検索が使用できない場合でもチャンネルのノート検索欄がでていた問題を修正
+- Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正
+- Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正
+ (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4)
+- Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正
+- Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正
+- Fix: MacOSでChrome系ブラウザを使用している場合に、Misskeyを閉じた際に他のタブのオーディオ機能と干渉する問題を修正
+- Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正
+- Fix: 「削除して編集」でノートの引用を解除出来なかった問題を修正( #14476 )
+- Fix: RSSウィジェットが正しく表示されない問題を修正
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/857)
+- Fix: ワードミュートの保存失敗時にAPIエラーが握りつぶされる事があるのを修正
+- Fix: アンケートでリモートの絵文字が正しく描画できない問題の修正
+ (Cherry-picked from https://github.com/yojo-art/cherrypick/pull/153)
+- Fix: 非ログイン時のサーバー概要画面のメニューボタンが押せないことがあるのを修正
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/656)
+- Fix: URLにはじめから`#pswp`が含まれている場合に画像ビューワーがブラウザの戻るボタンで閉じられない問題を修正
+- Fix: ロール作成画面で設定できるアイコンデコレーションの最大取付個数を16に制限
+- Fix: Firefox Nightlyなどでアイコンが読み込めない問題を修正
+
+### Server
+- Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように
+- Enhance: ノート検索の選択肢としてpgroongaに対応 ( #14730 )
+- Enhance: チャート更新時にDBに同時接続しないように
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830)
+- Enhance: config(default.yml)からSQLログ全文を出力するか否かを設定可能に ( #15266 )
+- Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 )
+- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737)
+- Fix: ノートの閲覧にログイン必須にしてもFeedでノートが表示されてしまう問題を修正
+- Fix: 絵文字の連合でライセンス欄を相互にやり取りするように ( #10859, #14109 )
+- Fix: ロックダウンされた期間指定のノートがStreaming経由でLTLに出現するのを修正 ( #15200 )
+- Fix: disableClustering設定時の初期化ロジックを調整( #15223 )
+- Fix: URLとURIが異なるエンティティの照会に失敗する問題を修正( #15039 )
+- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正
+ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869)
+- Fix: `/api/pages/update`にて`name`を指定せずにリクエストするとエラーが発生する問題を修正
+- Fix: AIセンシティブ判定が arm64 環境で動作しない問題を修正
+- Fix: 非Misskey系のソフトウェアからHTML`<ruby>`タグを含むノートを受信した場合、MFMの読み仮名(ルビ)文法に変換して表示
+- Fix: 連合OFFで投稿されたノートに対する冗長な処理を抑止 ( #15018 )
+- Fix: `/api.json`のレスポンスが2回目のリクエスト以降おかしくなる問題を修正
+
+### Misskey.js
+- Feat: allow setting `binaryType` of WebSocket connection
+
## 2024.11.0
### Note
@@ -37,7 +137,7 @@
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
- Fix: リンク切れを修正
-= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
+- Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6b01135d11..76f9d7b613 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -204,25 +204,10 @@ pnpm dev
command.
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
-- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
- Service Worker is watched by esbuild.
-- The front end can be viewed by accessing `http://localhost:5173`.
-- The backend listens on the port configured with `port` in .config/default.yml.
-If you have not changed it from the default, it will be "http://localhost:3000".
-If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts.
-
-### `MK_DEV_PREFER=backend pnpm dev`
-pnpm dev has another mode with `MK_DEV_PREFER=backend`.
-
-```
-MK_DEV_PREFER=backend pnpm dev
-```
-
-- This mode is closer to the production environment than the default mode.
-- Vite runs behind the backend (the backend will proxy Vite at /vite).
+- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
+- Vite runs behind the backend (the backend will proxy Vite at /vite and /embed_vite except for websocket used for HMR).
- You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml).
-- To change the port of Vite, specify with `VITE_PORT` environment variable.
-- HMR may not work in some environments such as Windows.
## Testing
@@ -499,6 +484,11 @@ describe('test', () => {
コード上でMisskeyのドメイン固有の概念には`Mi`をprefixすることで、他のドメインの同様の概念と区別できるほか、名前の衝突を防ぐ。
ただし、文脈上Misskeyのものを指すことが明らかであり、名前の衝突の恐れがない場合は、一時的なローカル変数に限って`Mi`を省略してもよい。
+### Misskey.jsの型生成
+```bash
+pnpm build-misskey-js-with-types
+```
+
### How to resolve conflictions occurred at pnpm-lock.yaml?
Just execute `pnpm` to fix it.
@@ -670,23 +660,23 @@ seems to do a decent job)
*after that commit*, do all the extra work, on the same branch:
* copy all changes (commit after each step):
- * in
- `packages/backend/src/core/activitypub/models/ApNoteService.ts`,
- from `createNote` to `updateNote`
- * from `packages/backend/src/core/NoteCreateService.ts` to
- `packages/backend/src/core/NoteEditService.ts`
- * from `packages/backend/src/server/api/endpoints/notes/create.ts`
- to `packages/backend/src/server/api/endpoints/notes/edit.ts`
- * from `packages/frontend/src/components/MkNote*.vue` to
- `packages/frontend/src/components/SkNote*.vue` (if sensible)
- * from the global timeline to the bubble timeline
- (`packages/backend/src/server/api/stream/channels/global-timeline.ts`,
- `packages/backend/src/server/api/stream/channels/bubble-timeline.ts`,
- `packages/frontend/src/timelines.ts`,
- `packages/frontend/src/components/MkTimeline.vue`,
- `packages/frontend/src/pages/timeline.vue`,
- `packages/frontend/src/ui/deck/tl-column.vue`,
- `packages/frontend/src/widgets/WidgetTimeline.vue`)
+ * in
+ `packages/backend/src/core/activitypub/models/ApNoteService.ts`,
+ from `createNote` to `updateNote`
+ * from `packages/backend/src/core/NoteCreateService.ts` to
+ `packages/backend/src/core/NoteEditService.ts`
+ * from `packages/backend/src/server/api/endpoints/notes/create.ts`
+ to `packages/backend/src/server/api/endpoints/notes/edit.ts`
+ * from `packages/frontend/src/components/MkNote*.vue` to
+ `packages/frontend/src/components/SkNote*.vue` (if sensible)
+ * from the global timeline to the bubble timeline
+ (`packages/backend/src/server/api/stream/channels/global-timeline.ts`,
+ `packages/backend/src/server/api/stream/channels/bubble-timeline.ts`,
+ `packages/frontend/src/timelines.ts`,
+ `packages/frontend/src/components/MkTimeline.vue`,
+ `packages/frontend/src/pages/timeline.vue`,
+ `packages/frontend/src/ui/deck/tl-column.vue`,
+ `packages/frontend/src/widgets/WidgetTimeline.vue`)
* if there have been any changes to the federated user data (the
`renderPerson` function in
`packages/backend/src/core/activitypub/ApRendererService.ts`), make
@@ -702,23 +692,20 @@ seems to do a decent job)
build` (the `development` tells it to keep some of the original
filenames in the built files)
* make sure there aren't any new `ti-*` classes (Tabler Icons), and
- replace them with appropriate `ph-*` ones (Phosphor Icons):
- `grep -rP '["'\'']ti[ -](?!fw)' -- built/` should show you what to change.
- NOTE: `ti-fw` is a special class that's defined by Misskey, leave it
- alone
-
- after every change, re-build the frontend and check again, until
- there are no more `ti-*` classes in the built files (you can ignore
- the source maps)
-
- commit!
+ replace them with appropriate `ph-*` ones (Phosphor Icons) in
+ [`vite.replaceicons.ts`](packages/frontend/vite.replaceIcons.ts).
+ This command should show you want to change: `grep -ohrP
+ '(?<=["'\'']ti )(ti-(?!fw)[\w\-]+)' --exclude \*.map -- built/ |
+ sort -u`.
+ * NOTE: `ti-fw` is a special class that's defined by Misskey, leave it alone.
+ * After every change, re-build the frontend and check again, until
+ there are no more `ti-*` classes in the built files.
+ * Commit!
* double-check the new migration, that they won't conflict with our db
changes: `git diff develop -- packages/backend/migration/`
* `pnpm clean; pnpm build`
-* run tests `pnpm --filter='!megalodon' test; pnpm --filter backend
- test:e2e` (requires a test database, [see above](#testing)) and fix
- as much as you can
- * right now `megalodon` doesn't pass its tests, so we skip them
+* run tests `pnpm test; pnpm --filter backend test:e2e` (requires a
+ test database, [see above](#testing)) and fix as much as you can
* run lint `pnpm --filter=backend --filter=frontend-shared lint` +
`pnpm --filter=frontend --filter=frontend-embed eslint` and fix as
much as you can
diff --git a/COPYING b/COPYING
index 6a5f3ca1d5..7635bfc913 100644
--- a/COPYING
+++ b/COPYING
@@ -1,5 +1,5 @@
Unless otherwise stated this repository is
-Copyright © 2014-2024 syuilo and contributors
+Copyright © 2014-2025 syuilo and contributors
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
diff --git a/Dockerfile b/Dockerfile
index aff4074079..72f934b3ce 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ ARG NODE_VERSION=22.11.0-alpine3.20
FROM node:${NODE_VERSION} as build
-RUN apk add git linux-headers build-base
+RUN apk add git linux-headers alpine-sdk pixman pango cairo cairo-dev pango-dev pixman-dev
ENV PYTHONUNBUFFERED=1
ENV COREPACK_DEFAULT_TO_LATEST=0
@@ -38,7 +38,7 @@ ARG UID="991"
ARG GID="991"
ENV COREPACK_DEFAULT_TO_LATEST=0
-RUN apk add ffmpeg tini jemalloc \
+RUN apk add ffmpeg tini jemalloc pixman pango cairo libpng \
&& corepack enable \
&& addgroup -g "${GID}" sharkey \
&& adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \
diff --git a/SECURITY.md b/SECURITY.md
index 8d3d44db41..499fa2c1fa 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -8,6 +8,11 @@ bug report to the GitLab repository.
Thanks for helping make Sharkey safe for everyone.
+> [!note]
+> CNA [requires](https://www.cve.org/ResourcesSupport/AllResources/CNARules#section_5-2_Description) that CVEs include a description in English for inclusion in the CVE Catalog.
+>
+> When creating a security advisory, all content must be written in English (it is acceptable to include a non-English description along with the English one).
+
## When create a patch
If you can also create a patch to fix the vulnerability, please create a PR on the private fork.
diff --git a/chart/files/default.yml b/chart/files/default.yml
index 97201aad66..4f43d52ae0 100644
--- a/chart/files/default.yml
+++ b/chart/files/default.yml
@@ -200,6 +200,19 @@ id: "aidx"
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
+# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1)
+#maxNoteLength: 3000
+# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1)
+#maxRemoteNoteLength: 100000
+# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1)
+#maxCwLength: 500
+# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1)
+#maxRemoteCwLength: 5000
+# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1)
+#maxAltTextLength: 20000
+# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1)
+#maxRemoteAltTextLength: 100000
+
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
@@ -230,5 +243,30 @@ checkActivityPubGetSignature: false
# '127.0.0.1/32'
#]
+#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks']
+
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
+
+# timeout (in milliseconds) and maximum size for imports (e.g. note imports)
+#import:
+# downloadTimeout: 30000
+# maxFileSize: 262144000
+
+# PID File of master process
+#pidFile: /tmp/misskey.pid
+
+# CHMod-style permission bits to apply to uploaded files.
+# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
+# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
+#filePermissionBits: '644'
+
+# Log settings
+# logging:
+# sql:
+# # Outputs query parameters during SQL execution to the log.
+# # default: false
+# enableQueryParamLogging: false
+# # Disable query truncation. If set to true, the full text of the query will be output to the log.
+# # default: false
+# disableQueryTruncation: false
diff --git a/compose_example.yml b/compose_example.yml
index 0db8b04dc6..de13722edc 100644
--- a/compose_example.yml
+++ b/compose_example.yml
@@ -19,6 +19,8 @@ services:
- shonk
# env_file:
# - .config/docker.env
+ environment:
+ - NODE_OPTIONS="--max-old-space-size=8192"
volumes:
- ./files:/sharkey/files
- ./.config:/sharkey/.config:ro
@@ -37,7 +39,7 @@ services:
db:
restart: always
- image: postgres:15-alpine
+ image: groonga/pgroonga:4.0.1-alpine-17
networks:
- shonk
env_file:
@@ -82,7 +84,7 @@ services:
# meilisearch:
# restart: always
-# image: getmeili/meilisearch:v1.3.4
+# image: getmeili/meilisearch:v1.13.0
# environment:
# - MEILI_NO_ANALYTICS=true
# - MEILI_ENV=production
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 2f1b391b53..91c90ce75a 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -1460,9 +1460,6 @@ _pages:
newPage: "أنشئ صفحة جديدة"
editPage: "عدّل الصفحة"
readPage: "نُشّط عرض المصدر"
- created: "نجح إنشاء الصفحة"
- updated: "نجح تعديل الصفحة"
- deleted: "نجح حذف الصفحة"
pageSetting: "إعدادات الصفحة"
nameAlreadyExists: "رابط الصفحة موجود مسبقًا"
invalidNameTitle: "رابط الصفحة ليس صالحًا"
@@ -1584,3 +1581,6 @@ _reversi:
_offlineScreen:
title: "غير متصل - يتعذر الاتصال بالخادم"
header: "يتعذر الاتصال بالخادم"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "غير موجود"
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index 6cd577b4a9..709874ac20 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -1237,9 +1237,6 @@ _pages:
newPage: "নতুন পৃষ্ঠা বানান"
editPage: "পৃষ্ঠাটি সম্পাদনা করুন"
readPage: "উৎস দেখছেন"
- created: "পৃষ্ঠা তৈরি করা হয়েছে"
- updated: "পৃষ্ঠা সম্পাদনা করা হয়েছে"
- deleted: "পৃষ্ঠা মুছে ফেলা হয়েছে"
pageSetting: "পৃষ্ঠার সেটিংস"
nameAlreadyExists: "পৃষ্ঠার URLটি ইতিমধ্যেই ব্যাবহার করা হয়েছে"
invalidNameTitle: "পৃষ্ঠার URL অবৈধ"
@@ -1348,3 +1345,6 @@ _moderationLogTypes:
resetPassword: "পাসওয়ার্ড রিসেট করুন"
_reversi:
total: "মোট"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "পাওয়া যায়নি"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 1aca3390e6..7b029c6f41 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -5,6 +5,7 @@ introMisskey: "Benvingut! Misskey és un servei de microblogging descentralitzat
poweredByMisskeyDescription: "{name} És un dels serveis (anomenats instàncies de Misskey) que utilitzen la plataforma de codi obert <b>Misskey</b>."
monthAndDay: "{day}/{month}"
search: "Cercar"
+reset: "Reiniciar"
notifications: "Notificacions"
username: "Nom d'usuari"
password: "Contrasenya"
@@ -43,11 +44,12 @@ favorites: "Favorits"
unfavorite: "Eliminar dels preferits"
favorited: "Afegit als preferits."
alreadyFavorited: "Ja s'ha afegit als preferits."
-cantFavorite: "No s'ha pogut afegir als preferits."
-pin: "Fixar al perfil"
+cantFavorite: "No es pot afegir als preferits."
+pin: "Fixa al perfil"
unpin: "Para de fixar del perfil"
-copyContent: "Copiar el contingut"
-copyLink: "Copiar l'enllaç"
+copyContent: "Copia el contingut"
+copyLink: "Copia l'enllaç"
+copyRemoteLink: "Copiar l'enllaç remot"
copyLinkRenote: "Copiar l'enllaç de la renota"
delete: "Elimina"
deleteAndEdit: "Eliminar i editar"
@@ -103,9 +105,9 @@ enterListName: "Introdueix un nom per a la llista"
privacy: "Privadesa"
makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació"
defaultNoteVisibility: "Visibilitat per defecte"
-follow: "Seguint"
+follow: "Segueix"
followRequest: "Enviar sol·licitud de seguiment"
-followRequests: "Sol·licituds de seguiment"
+followRequests: "Peticions de seguiment"
unfollow: "Deixar de seguir"
followRequestPending: "Sol·licituds de seguiment pendents"
enterEmoji: "Introduir un emoji"
@@ -195,7 +197,7 @@ setWallpaper: "Defineix el fons de pantalla"
removeWallpaper: "Elimina el fons de pantalla"
searchWith: "Cerca: {q}"
youHaveNoLists: "No tens cap llista"
-followConfirm: "Estàs segur que vols deixar de seguir {name}?"
+followConfirm: "Segur que vols seguir a {name}?"
proxyAccount: "Compte de proxy"
proxyAccountDescription: "Un compte proxy és un compte que actua com a seguidor remot per als usuaris en determinades condicions. Per exemple, quan un usuari afegeix un usuari remot a la llista, l'activitat de l'usuari remot no es lliurarà al servidor si cap usuari local segueix aquest usuari, de manera que el compte proxy el seguirà."
host: "Amfitrió"
@@ -222,7 +224,7 @@ version: "Versió"
metadata: "Metadades"
withNFiles: "{n} fitxer(s)"
monitor: "Monitor"
-jobQueue: "Cua de tasques"
+jobQueue: "Cua de feines"
cpuAndMemory: "CPU i memòria"
network: "Xarxa"
disk: "Disc"
@@ -325,7 +327,7 @@ dark: "Fosc"
lightThemes: "Temes clars"
darkThemes: "Temes foscos"
syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu"
-drive: "Unitat"
+drive: "Disc"
fileName: "Nom del Fitxer"
selectFile: "Selecciona un fitxer"
selectFiles: "Selecciona fitxers"
@@ -340,11 +342,11 @@ deleteFolder: "Elimina la carpeta"
folder: "Carpeta "
addFile: "Afegeix un fitxer"
showFile: "Mostrar fitxer"
-emptyDrive: "La teva unitat és buida"
+emptyDrive: "El teu Disc és buit"
emptyFolder: "La carpeta està buida"
unableToDelete: "No es pot eliminar"
inputNewFileName: "Introduïu el nom de fitxer nou"
-inputNewDescription: "Inserta una nova llegenda"
+inputNewDescription: "Escriu el peu de foto."
inputNewFolderName: "Introduïu el nom de la carpeta nova"
circularReferenceFolder: "La carpeta destinatària és una subcarpeta de la carpeta a la qual la desitges moure"
hasChildFilesOrFolders: "No és possible esborrar aquesta carpeta ja que no és buida"
@@ -537,7 +539,7 @@ mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única
limitTo: "Limita a {x}"
noFollowRequests: "No tens sol·licituds de seguiment"
openImageInNewTab: "Obre imatges a una nova pestanya"
-dashboard: "Panell de control"
+dashboard: "Taulell de control"
local: "Local"
remote: "Remot"
total: "Total"
@@ -573,7 +575,7 @@ serverLogs: "Registres del servidor"
deleteAll: "Elimina-ho tot"
showFixedPostForm: "Mostrar el formulari per escriure a l'inici de la línia de temps"
showFixedPostFormInChannel: "Mostrar el formulari d'escriptura al principi de la línia de temps (Canals)"
-withRepliesByDefaultForNewlyFollowed: "Inclou les respostes d'usuaris nous seguits a la línia de temps per defecte."
+withRepliesByDefaultForNewlyFollowed: "Inclou les respostes d'usuaris nous que segueixes a la línia de temps per defecte."
newNoteRecived: "Hi ha publicacions noves"
sounds: "Sons"
sound: "So"
@@ -614,7 +616,7 @@ unsetUserBanner: "Desactiva el bàner "
unsetUserBannerConfirm: "Segur que vols desactivar el bàner?"
deleteAllFiles: "Esborra tots els arxius"
deleteAllFilesConfirm: "Segur que vols esborrar tots els arxius?"
-removeAllFollowing: "Deixa de seguir tots els usuaris seguits"
+removeAllFollowing: "Deixa de seguir tots els usuaris que segueixes"
removeAllFollowingDescription: "El fet d'executar això, et farà deixar de seguir a tots els usuaris de {host}. Si us plau, executa això si l'amfitrió, per exemple, ja no existeix."
userSuspended: "Aquest usuari ha sigut suspès"
userSilenced: "Aquest usuari està sent silenciat"
@@ -645,7 +647,7 @@ expandTweet: "Expandir post"
themeEditor: "Editor de temes"
description: "Descripció"
describeFile: "Afegir subtitulació"
-enterFileDescription: "Afegeix un títol"
+enterFileDescription: "Escriu un peu de foto"
author: "Autor"
leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?"
manage: "Administració"
@@ -684,11 +686,15 @@ smtpSecure: "Fes servir SSL/TLS per connexions SMTP"
smtpSecureInfo: "Desactiva això quan facis servir connexions STARTTLS"
testEmail: "Prova l'enviament de correu "
wordMute: "Silenciar paraules "
+wordMuteDescription: "Minimitza les notes que contenen la paraula o frase especificada. Les notes minimitzades poden visualitzar-se fent clic sobre elles."
hardWordMute: "Silenciar paraules fortes"
+showMutedWord: "Mostrar paraules silenciades"
+hardWordMuteDescription: "Oculta les notes que contenen la paraula o frase especificada. A diferència de Silenciar paraula, la nota quedarà completament oculta a la vista."
regexpError: "Error de l'expressió regular "
regexpErrorDescription: "S'ha produït un error a l'expressió regular a la línia {line} de les paraules silenciades {tab}:"
instanceMute: "Silenciar servidor"
userSaysSomething: "{name} n'ha dit alguna cosa"
+userSaysSomethingAbout: "{name} està parlant sobre \"{word}\""
makeActive: "Activar"
display: "Veure"
copy: "Copiar"
@@ -729,7 +735,7 @@ instanceTicker: "Informació de notes de la instància "
waitingFor: "Esperant {x}"
random: "Aleatori "
system: "Sistema"
-switchUi: "Canviar interfície d'usuari "
+switchUi: "Canviar la interfície"
desktop: "Escriptori"
clip: "Retalls"
createNew: "Crear"
@@ -747,7 +753,7 @@ repliesCount: "Nombre de respostes"
renotesCount: "Impulsos fets"
repliedCount: "Nombre de respostes rebudes"
renotedCount: "Impulsos rebuts"
-followingCount: "Nombre de comptes seguits"
+followingCount: "Nombre de comptes que segueixes"
followersCount: "Nombre de seguidors"
sentReactionsCount: "Nombre de reaccions enviades"
receivedReactionsCount: "Nombre de reaccions rebudes"
@@ -779,7 +785,7 @@ thisIsExperimentalFeature: "Aquesta és una característica experimental. La sev
developer: "Programador"
makeExplorable: "Fes que el compte sigui visible a la secció \"Explorar\""
makeExplorableDescription: "Si desactives aquesta opció, el teu compte no sortirà a la secció \"Explorar\""
-showGapBetweenNotesInTimeline: "Mostra una separació entre els articles a la línia de temps"
+showGapBetweenNotesInTimeline: "Notes separades a la línia de temps"
duplicate: "Duplicat"
left: "Esquerra"
center: "Centre"
@@ -910,7 +916,7 @@ off: "Desactivar"
emailRequiredForSignup: "Demanar correu electrònic per registrar-se "
unread: "Sense llegir"
filter: "Filtrar"
-controlPanel: "Panel de control"
+controlPanel: "Taulell de control"
manageAccounts: "Gestionar comptes"
makeReactionsPublic: "Reaccions públiques "
makeReactionsPublicDescription: "Això fa que totes les teves reaccions siguin visibles públicament "
@@ -929,7 +935,7 @@ useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calai
welcomeBackWithName: "Benvingut de nou, {name}"
clickToFinishEmailVerification: "Si us plau, fes clic a [{ok}] per completar la verificació per correu electrònic "
overridedDeviceKind: "Tipus de dispositiu"
-smartphone: "Telèfon intel·ligent"
+smartphone: "Mòbil "
tablet: "Tauleta"
auto: "Automàtic "
themeColor: "Color del tema"
@@ -1010,7 +1016,7 @@ sendPushNotificationReadMessageCaption: "Això pot fer que el teu dispositiu con
windowMaximize: "Maximitzar "
windowMinimize: "Minimitzar"
windowRestore: "Restaurar"
-caption: "Llegenda"
+caption: "Peu de foto"
loggedInAsBot: "Identificat com a bot"
tools: "Eines"
cannotLoad: "No es pot carregar"
@@ -1133,7 +1139,7 @@ channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista d
thisChannelArchived: "Aquest Canal ha sigut arxivat."
displayOfNote: "Mostrar notes"
initialAccountSetting: "Configuració del perfil"
-youFollowing: "Seguit"
+youFollowing: "Seguint"
preventAiLearning: "Descartar l'ús d'aprenentatge automàtic (IA Generativa)"
preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automàtic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta."
options: "Opcions"
@@ -1199,7 +1205,7 @@ showRenotes: "Mostrar impulsos"
edited: "Editat"
notificationRecieveConfig: "Paràmetres de notificacions"
mutualFollow: "Seguidor mutu"
-followingOrFollower: "Seguit o seguidor"
+followingOrFollower: "Seguint o seguidor"
fileAttachedOnly: "Només notes amb adjunts"
showRepliesToOthersInTimeline: "Mostrar les respostes a altres a la línia de temps"
hideRepliesToOthersInTimeline: "Amagar les respostes a altres a la línia de temps"
@@ -1301,6 +1307,8 @@ lockdown: "Bloquejat"
pleaseSelectAccount: "Seleccionar un compte"
availableRoles: "Roles disponibles "
acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perills."
+federationSpecified: "Aquest servidor treballa amb una federació de llistes blanques. No pot interactuar amb altres servidors que no siguin els especificats per l'administrador."
+federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors."
_accountSettings:
requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut"
requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació."
@@ -1473,7 +1481,7 @@ _accountMigration:
startMigration: "Migrar"
migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podràs tornar a fer servir aquest compte mai més."
movedAndCannotBeUndone: "Aquest compte ha migrat.\nLes migracions no es poden desfer."
- postMigrationNote: "Aquest compte deixarà de seguir tots els comptes que segueix 24 hores després de germinar la migració.\nEl nombre de seguidors i seguits passarà a ser de zero. Per evitar que els teus seguidors no puguin veure les publicacions marcades com a només seguidors continuaren seguint aquest compte."
+ postMigrationNote: "Aquest compte deixarà de seguir tots els comptes que segueix 24 hores després de terminar la migració.\nEl nombre de seguidors i seguits passarà a ser de zero. Per evitar que els teus seguidors no puguin veure les publicacions marcades com a només seguidors continuaren seguint aquest compte."
movedTo: "Nou compte:"
_achievements:
earnedAt: "Desbloquejat el"
@@ -1832,7 +1840,7 @@ _emailUnavailable:
smtp: "Aquest servidor de correu electrònic no respon"
banned: "No pots registrar-te amb aquesta adreça de correu electrònic "
_ffVisibility:
- public: "Publicar"
+ public: "Públic "
followers: "Visible només per a seguidors "
private: "Privat"
_signup:
@@ -1866,7 +1874,7 @@ _gallery:
unlike: "Ja no m'agrada"
_email:
_follow:
- title: "t'ha seguit"
+ title: "Tens un nou seguidor"
_receiveFollowRequest:
title: "Has rebut una sol·licitud de seguiment"
_plugin:
@@ -1900,7 +1908,7 @@ _registry:
domain: "Domini"
createKey: "Crear una clau"
_aboutMisskey:
- about: "Misskey és un programa de codi obert desenvolupar per syuilo des de 2014"
+ about: "Misskey és un programa de codi obert desenvolupat des del 2014 per syuilo"
contributors: "Col·laboradors principals"
allContributors: "Tots els col·laboradors "
source: "Codi font"
@@ -2219,7 +2227,7 @@ _widgets:
slideshow: "Presentació"
button: "Botó "
onlineUsers: "Usuaris actius"
- jobQueue: "Cua de tasques"
+ jobQueue: "Cua de feines"
serverMetric: "Mètriques del servidor"
aiscript: "Consola AiScript"
aiscriptApp: "Aplicació AiScript"
@@ -2299,7 +2307,7 @@ _exportOrImport:
allNotes: "Totes les publicacions"
favoritedNotes: "Notes preferides"
clips: "Retalls"
- followingList: "Seguint"
+ followingList: "Seguint "
muteList: "Silencia"
blockingList: "Bloqueja"
userLists: "Llistes"
@@ -2357,9 +2365,6 @@ _pages:
newPage: "pa"
editPage: "Editar la pàgina"
readPage: "Veure el codi font d'aquesta pàgina"
- created: "La pàgina ha sigut creada correctament"
- updated: "La pàgina s'ha editat correctament"
- deleted: "La pàgina s'ha esborrat sense problemes"
pageSetting: "Configuració de la pàgina"
nameAlreadyExists: "L'adreça URL de la pàgina ja existeix"
invalidNameTitle: "L'adreça URL de la pàgina no és vàlida"
@@ -2438,7 +2443,7 @@ _notification:
_types:
all: "Tots"
note: "Notes noves"
- follow: "Seguint"
+ follow: "Segueix-me"
mention: "Menció"
reply: "Respostes"
renote: "Renotar"
@@ -2454,7 +2459,7 @@ _notification:
test: "Prova la notificació"
app: "Notificacions d'aplicacions"
_actions:
- followBack: "t'ha seguit també"
+ followBack: "També et segueix"
reply: "Respondre"
renote: "Renotar"
_deck:
@@ -2721,6 +2726,66 @@ _contextMenu:
app: "Aplicació "
appWithShift: "Aplicació amb la tecla shift"
native: "Interfície del navegador"
+_gridComponent:
+ _error:
+ requiredValue: "Aquest camp és obligatori"
+ columnTypeNotSupport: "La validació d'expressions regulars només s'admet per columnes de tipus text."
+ patternNotMatch: "Aquest valor no coincideix amb {pattern}"
+ notUnique: "Aquest valor ha de ser únic "
+_roleSelectDialog:
+ notSelected: "No seleccionat"
+_customEmojisManager:
+ _gridCommon:
+ copySelectionRows: "Copiar línies seleccionades "
+ copySelectionRanges: "Copiar selecció "
+ deleteSelectionRows: "Esborrar línies seleccionades"
+ deleteSelectionRanges: "Esborrar files de la selecció "
+ searchSettings: "Configuració del cercador"
+ searchSettingCaption: "Defineix criteris de cerca detallats."
+ searchLimit: "Nombre de pantalles"
+ sortOrder: "Ordenar"
+ registrationLogs: "Registres d'inscripcions "
+ registrationLogsCaption: "Quan s'actualitzin o s'esborrin emojis es mostrarà un registre. Desapareixeran quan s'actualitzin, s'esborrin, visitis una nova pàgina o la recarreguis."
+ alertEmojisRegisterFailedDescription: "No s'ha pogut actualitzar o esborrar l'emoji. Si us plau, dona una ullada al registre per més detalls."
+ _logs:
+ showSuccessLogSwitch: "Mostrar el registre d'èxit "
+ failureLogNothing: "No hi ha registres de fallades."
+ logNothing: "No hi ha registres."
+ _remote:
+ selectionRowDetail: "Detall de la línia seleccionada"
+ importSelectionRows: "Importar les files seleccionades"
+ importSelectionRangesRows: "Importar les files de la selecció "
+ importEmojisButton: "Importar els Emojis marcats"
+ confirmImportEmojisTitle: "Importar Emojis"
+ confirmImportEmojisDescription: "Importar {count} Emojis d'una adreça remota. Tingues cura de les llicències dels Emojis. Vols importar-los?"
+ _local:
+ tabTitleList: "Llistar els Emojis registrats"
+ tabTitleRegister: "Registre d'Emojis"
+ _list:
+ emojisNothing: "No hi ha Emojis registrats"
+ markAsDeleteTargetRows: "Files seleccionades que s'han d'esborrar "
+ markAsDeleteTargetRanges: "Selecció de files per la seva eliminació "
+ alertUpdateEmojisNothingDescription: "No hi ha Emojis actualitzats."
+ alertDeleteEmojisNothingDescription: "No hi ha Emoji per esborrar."
+ confirmMovePage: "Vols canviar de pàgina?"
+ confirmChangeView: "Vols canviar la pantalla?"
+ confirmUpdateEmojisDescription: "Actualitzar {count} Emojis. Vols executar-ho?"
+ confirmDeleteEmojisDescription: "Esborrar {count} Emojis marcats. Vols continuar?"
+ confirmResetDescription: "Es restabliran tots els canvis fets fins ara."
+ confirmMovePageDesciption: "S'han fet canvis als Emojis d'aquesta pàgina. Si continues navegant sense guardar els canvis, es perdran tots els canvis fets en aquesta pàgina."
+ dialogSelectRoleTitle: "Buscar Emojis per rol"
+ _register:
+ uploadSettingTitle: "Actualitza la configuració "
+ uploadSettingDescription: "En aquesta pantalla pots configurar el que s'ha de fer quan es puja un Emoji."
+ directoryToCategoryLabel: "Escriu el nom del directori al camp de \"categoria\""
+ directoryToCategoryCaption: "Quan arrossegues un directori, escriu el nom del directori al camp categoria."
+ emojiInputAreaCaption: "Selecciona els Emojis que vols registrar gent servir un dels mètodes."
+ emojiInputAreaList1: "Arrossega i deixar anar fitxers o directoris dintre del quadrat."
+ emojiInputAreaList2: "Clica l'enllaç per seleccionar un fitxer des del teu ordinador."
+ emojiInputAreaList3: "Clica aquest enllaç per seleccionar del Disc"
+ confirmRegisterEmojisDescription: "Registrar els Emojis de la llista com a nous Emojis personalitzats. Vols continuar? (Per evitar una sobrecàrrega només {count} Emojis es poden registrar d'una sola vegada)"
+ confirmClearEmojisDescription: "Descartar els canvis i esborrar els Emojis de la llista. Vols continuar?"
+ confirmUploadEmojisDescription: "Pujar els {count} fitxers que has arrossegat al disc. Vols continuar?"
_embedCodeGen:
title: "Personalitza el codi per incrustar"
header: "Mostrar la capçalera"
@@ -2744,3 +2809,34 @@ _selfXssPrevention:
_followRequest:
recieved: "Sol·licituds rebudes"
sent: "Sol·licituds enviades"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "No es pot establir connexió amb aquest servidor"
+ description: "És possible que s'hagi desactivat la comunicació amb aquest servidor o que hagi estat bloquejat.\nPosa't en contacte amb l'administrador del servidor."
+ _uriInvalid:
+ title: "L'adreça és incorrecte"
+ description: "Hi ha un problema amb l'adreça introduïda; comprova que no hagis escrit caràcters que no es puguin fer servir."
+ _requestFailed:
+ title: "La sol·licitud a fallat"
+ description: "La comunicació amb aquest servidor a fallat. És possible que l'altre servidor no funcioni. Comprova també que no has posat una adreça no vàlida o inexistent."
+ _responseInvalid:
+ title: "La resposta no és correcta "
+ description: "Hem pogut comunicar-nos amb aquest servidor, però les dades rebudes no són correctes."
+ _responseInvalidIdHostNotMatch:
+ description: "El domini de l'adreça introduïda no és el mateix que el domini de l'adreça final obtinguda. Si estàs consultant continguts remots mitjançant servidors tercers, torna a fer la consulta fent servir l'adreça que es pot obtenir en el servidor origen."
+ _noSuchObject:
+ title: "No s'ha trobat"
+ description: "No es pot trobar el recurs sol·licitat, si us plau comprova l'adreça una altra vegada."
+_captcha:
+ verify: "Passar pel CAPTCHA"
+ testSiteKeyMessage: "Pots comprovar una vista prèvia introduïnt valors de prova per la clau del lloc i la clau secreta. Si vols més informació consulteu la següent pàgina."
+ _error:
+ _requestFailed:
+ title: "Ha fallat la sol·licitud del CAPTCHA"
+ text: "Si us plau, torna a intentar-ho d'aquí una estona o comprova els ajustos de nou."
+ _verificationFailed:
+ title: "Ha fallat la validació CAPTCHA"
+ text: "Comprova que els ajustos són els correctes."
+ _unknown:
+ title: "Error CAPTCHA"
+ text: "S'ha produït un error inesperat."
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index 504ba1f8c8..afa3047c1d 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -1883,9 +1883,6 @@ _pages:
newPage: "Vytvořit novou stránku"
editPage: "Upravit stránku"
readPage: "Prohlížení zdroje této stránky"
- created: "Stránka byla úspěšně vytvořena"
- updated: "Stránka byla úspěšně aktualizována"
- deleted: "Stránka byla úspěšně smazána"
pageSetting: "Nastavení stránky"
nameAlreadyExists: "Zadaná adresa URL stránky již existuje"
invalidNameTitle: "Zadaná adresa URL stránky je neplatná"
@@ -2024,3 +2021,6 @@ _moderationLogTypes:
createInvitation: "Vygenerovat pozvánku"
_reversi:
total: "Celkem"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Nenalezeno"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index d85c930b73..11fe6d3ff5 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -5,6 +5,7 @@ introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microbl
poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform <b>Misskey</b> betriebenen Dienste."
monthAndDay: "{day}.{month}."
search: "Suchen"
+reset: "Zurücksetzen"
notifications: "Benachrichtigungen"
username: "Benutzername"
password: "Passwort"
@@ -48,6 +49,7 @@ pin: "An dein Profil anheften"
unpin: "Von deinem Profil lösen"
copyContent: "Inhalt kopieren"
copyLink: "Link kopieren"
+copyRemoteLink: "Renote-Link kopieren"
copyLinkRenote: "Renote-Link kopieren"
delete: "Löschen"
deleteAndEdit: "Löschen und Bearbeiten"
@@ -185,7 +187,9 @@ addAccount: "Benutzerkonto hinzufügen"
reloadAccountsList: "Benutzerkontoliste aktualisieren"
loginFailed: "Anmeldung fehlgeschlagen"
showOnRemote: "Auf Ursprungsinstanz ansehen"
+continueOnRemote: "Weiter auf Remote-Server"
chooseServerOnMisskeyHub: "Wähle einen Server aus dem Misskey Hub"
+specifyServerHost: "Server-Host auswählen"
inputHostName: "Gib die Domain an"
general: "Allgemein"
wallpaper: "Hintergrund"
@@ -237,6 +241,8 @@ silencedInstances: "Stummgeschaltete Instanzen"
silencedInstancesDescription: "Gib die Hostnamen der Instanzen, welche stummgeschaltet werden sollen, durch Zeilenumbrüche getrennt an. Alle Konten dieser Instanzen werden als stummgeschaltet behandelt, können nur noch Follow-Anfragen stellen und wenn nicht gefolgt keine lokalen Konten erwähnen. Blockierte Instanzen sind davon nicht betroffen."
mediaSilencedInstances: "Medien-stummgeschaltete Server"
mediaSilencedInstancesDescription: "Gib pro Zeile die Hostnamen der Server ein, dessen Medien du stummschalten möchtest. Alle Benutzerkonten der aufgeführten Server werden als sensibel behandelt und können keine benutzerdefinierten Emojis verwenden. Gesperrte Server sind davon nicht betroffen."
+federationAllowedHosts: "Föderierte Instanzen"
+federationAllowedHostsDescription: "Trage die Hostnamen ein mit den du eine Föderation eingehen möchtest. Trenne mit Zeilenumbruch."
muteAndBlock: "Stummschaltungen und Blockierungen"
mutedUsers: "Stummgeschaltete Benutzer"
blockedUsers: "Blockierte Benutzer"
@@ -449,6 +455,7 @@ totpDescription: "Logge dich via Authentifizierungs-App mit Einmalpasswort ein"
moderator: "Moderator"
moderation: "Moderation"
moderationNote: "Moderationsnotiz"
+moderationNoteDescription: "Trage hier Notizen ein. Diese sind nur für die Moderatoren sichtbar."
addModerationNote: "Moderationsnotiz hinzufügen"
moderationLogs: "Moderationsprotokolle"
nUsersMentioned: "Von {n} Benutzern erwähnt"
@@ -488,6 +495,7 @@ noMessagesYet: "Noch keine Nachrichten vorhanden"
newMessageExists: "Du hast eine neue Nachricht"
onlyOneFileCanBeAttached: "Es kann pro Nachricht nur eine Datei angehängt werden"
signinRequired: "Bitte registriere oder melde dich an, um fortzufahren"
+signinOrContinueOnRemote: "Um fortzufahren, gehe zu deiner Instanz oder registriere bzw. melde dich an dieser Instanz an. "
invitations: "Einladungen"
invitationCode: "Einladungscode"
checking: "Wird überprüft …"
@@ -511,6 +519,7 @@ emojiStyle: "Emoji-Stil"
native: "Nativ"
menuStyle: "Menü Stil"
style: "Stil"
+drawer: "App-Übersicht"
popup: "Pop-up"
showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
showReactionsCount: "Zeige die Anzahl der Reaktionen auf Notizen an"
@@ -579,6 +588,7 @@ masterVolume: "Gesamtlautstärke"
notUseSound: "Gebe kein Ton aus"
useSoundOnlyWhenActive: "Gebe nur Ton aus, wenn Misskey aktiv ist"
details: "Details"
+renoteDetails: "Renote Details"
chooseEmoji: "Emoji auswählen"
unableToProcess: "Der Vorgang konnte nicht abgeschlossen werden"
recentUsed: "Vor kurzem verwendet"
@@ -595,6 +605,7 @@ descendingOrder: "Absteigende Reihenfolge"
scratchpad: "Testumgebung"
scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Misskey überprüfen."
uiInspector: "UI-Inspektor"
+uiInspectorDescription: "Die Liste der UI-Komponenten-Server können im Zwischenspeicher angesehen werden. Die UI-Komponente wird von der Funktion Ui:C: generiert."
output: "Ausgabe"
script: "Skript"
disablePagesScript: "AiScript auf Seiten deaktivieren"
@@ -675,11 +686,15 @@ smtpSecure: "Für SMTP-Verbindungen implizit SSL/TLS verwenden"
smtpSecureInfo: "Schalte dies aus, falls du STARTTLS verwendest."
testEmail: "Emailversand testen"
wordMute: "Wortstummschaltung"
-hardWordMute: "Harte Wort-Stummschaltung"
+wordMuteDescription: "Minimiert Notizen, die das angegebene Wort oder den angegebenen Ausdruck enthalten. Minimierte Notizen können angezeigt werden, indem du auf sie klickst."
+hardWordMute: "Harte Wortstummschaltung"
+showMutedWord: "Stummgeschaltete Wörter anzeigen"
+hardWordMuteDescription: "Blendet Notizen aus, die das angegebene Wort oder die angegebene Phrase enthalten. Im Gegensatz zur Wortstummschaltung wird die Notiz vollständig ausgeblendet."
regexpError: "Fehler in einem regulären Ausdruck"
regexpErrorDescription: "Im regulären Ausdruck deiner in Zeile {line} von {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:"
instanceMute: "Instanzstummschaltungen"
userSaysSomething: "{name} hat etwas gesagt"
+userSaysSomethingAbout: "{name} sagt etwas über '{word}'"
makeActive: "Aktivieren"
display: "Anzeigeart"
copy: "Kopieren"
@@ -848,6 +863,7 @@ administration: "Verwaltung"
accounts: "Benutzerkonten"
switch: "Wechseln"
noMaintainerInformationWarning: "Betreiberinformationen sind nicht konfiguriert."
+noInquiryUrlWarning: "Keine gültige URL."
noBotProtectionWarning: "Schutz vor Bots ist nicht konfiguriert."
configure: "Konfigurieren"
postToGallery: "Neuen Galeriebeitrag erstellen"
@@ -1080,12 +1096,15 @@ retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen."
enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen"
enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen"
+enableStatsForFederatedInstances: "Abruf von Informationen über förderierte Server"
showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen"
reactionsDisplaySize: "Reaktionsanzeigegröße"
limitWidthOfReaction: "Begrenze die Breite der Reaktion und zeige sie verkleinert an"
noteIdOrUrl: "Notiz-ID oder URL"
video: "Video"
videos: "Videos"
+audio: "Audio"
+audioFiles: "Audio"
dataSaver: "Datensparmodus"
accountMigration: "Kontomigration"
accountMoved: "Dieser Benutzer ist zu einem neuen Konto migriert:"
@@ -1125,6 +1144,9 @@ preventAiLearning: "Verwendung in machinellem Lernen (Generative bzw. Prediktive
preventAiLearningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (Generative bzw. Prediktive AI/KI) zu verwenden. Dies wird durch das Hinzufügen einer \"noai\"-Flag in der HTML-Antwort des jeweiligen Inhalts erreicht. Da diese Flag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich."
options: "Optionen"
specifyUser: "Spezifischer Benutzer"
+lookupConfirm: "Zustimmen?"
+openTagPageConfirm: "Hashtag Seite wirklich öffnen?"
+specifyHost: "Host"
failedToPreviewUrl: "Vorschau nicht anzeigbar"
update: "Aktualisieren"
rolesThatCanBeUsedThisEmojiAsReaction: "Rollen, die dieses Emoji als Reaktion verwenden können"
@@ -1183,6 +1205,7 @@ showRenotes: "Renotes anzeigen"
edited: "Bearbeitet"
notificationRecieveConfig: "Benachrichtigungseinstellungen"
mutualFollow: "Gegenseitig gefolgt"
+followingOrFollower: "Follow oder Follower"
fileAttachedOnly: "Nur Notizen mit Dateien"
showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen"
hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen"
@@ -1194,7 +1217,10 @@ externalServices: "Externe Dienste"
sourceCode: "Quellcode"
sourceCodeIsNotYetProvided: "Der Quellcode ist noch nicht verfügbar. Kontaktiere den Administrator, um das Problem zu lösen."
repositoryUrl: "Repository URL"
+repositoryUrlDescription: "Solltest du Misskey so wie es ist verwenden (im unveränderten Quellcode), gebe Folgendes an:\nhttps://github.com/misskey-dev/misskey"
repositoryUrlOrTarballRequired: "Wenn du kein Repository veröffentlicht hast, musst du stattdessen einen Tarball bereitstellen. Siehe .config/example.yml für weitere Informationen."
+feedback: "Feedback"
+feedbackUrl: "Feedback-Website"
impressum: "Impressum"
impressumUrl: "Impressums-URL"
impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend."
@@ -1204,6 +1230,7 @@ tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung"
avatarDecorations: "Profilbilddekoration"
attach: "Anbringen"
detach: "Entfernen"
+detachAll: "Alles Entfernen"
angle: "Winkel"
flip: "Umdrehen"
showAvatarDecorations: "Profilbilddekoration anzeigen"
@@ -1216,18 +1243,27 @@ signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen.
cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
doReaction: "Reagieren"
code: "Code"
+reloadRequiredToApplySettings: "Eine Aktualisierung ist erforderlich, um die Einstellungen zu übernehmen."
remainingN: "Verbleibend: {n}"
overwriteContentConfirm: "Bist du sicher, dass du den aktuellen Inhalt überschreiben willst?"
seasonalScreenEffect: "Saisonaler Bildschirmeffekt"
decorate: "Dekorieren"
addMfmFunction: "MFM hinzufügen"
enableQuickAddMfmFunction: "Erweiterte MFM-Auswahl anzeigen"
+bubbleGame: "Bubble Game"
sfx: "Soundeffekte"
soundWillBePlayed: "Es wird Ton wiedergegeben"
showReplay: "Wiederholung anzeigen"
+replay: "Aufzeichnen"
+replaying: "Aufzeichnung"
+endReplay: "Aufzeichnung verlassen"
+copyReplayData: "Aufzeichnung kopieren"
ranking: "Rangliste"
lastNDays: "Letzten {n} Tage"
backToTitle: "Zurück zum Startbildschirm"
+hemisphere: "Hemisphäre"
+withSensitive: "Zeige \"sensitive Inhalte\" an"
+userSaysSomethingSensitive: "{name} sagt etwas mit sensiblem Inhalt."
enableHorizontalSwipe: "Wischen, um zwischen Tabs zu wechseln"
loading: "Laden"
surrender: "Abbrechen"
@@ -1240,6 +1276,8 @@ useNativeUIForVideoAudioPlayer: "Browser-Benutzeroberfläche für die Video- und
keepOriginalFilename: "Ursprünglichen Dateinamen beibehalten"
keepOriginalFilenameDescription: "Wenn diese Einstellung deaktiviert ist, wird der Dateiname beim Hochladen automatisch durch eine zufällige Zeichenfolge ersetzt."
noDescription: "Keine Beschreibung vorhanden"
+alwaysConfirmFollow: "Folgen immer bestätigen"
+inquiry: "Kontakt"
tryAgain: "Bitte später erneut versuchen"
confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen"
sensitiveMediaRevealConfirm: "Es könnte sich um sensible Medien handeln. Möchtest du sie anzeigen?"
@@ -1249,29 +1287,41 @@ fromX: "Von {x}"
genEmbedCode: "Einbettungscode generieren"
noteOfThisUser: "Notizen dieses Benutzers"
clipNoteLimitExceeded: "Zu diesem Clip können keine weiteren Notizen hinzugefügt werden."
+performance: "Leistung"
+modified: "Bearbeitet"
discard: "Verwerfen"
thereAreNChanges: "Es gibt {n} Änderung(en)"
signinWithPasskey: "Mit Passkey anmelden"
+unknownWebAuthnKey: "Unbekannter Passkey"
passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert."
messageToFollower: "Nachricht an die Follower"
+target: "Speicherort"
testCaptchaWarning: "Diese Funktion ist für CAPTCHA-Testzwecke gedacht.\n<strong>Nicht in einer Produktivumgebung verwenden.</strong>"
prohibitedWordsForNameOfUser: "Verbotene Begriffe für Benutzernamen"
prohibitedWordsForNameOfUserDescription: "Wenn eine Zeichenfolge aus dieser Liste im Namen eines Benutzers enthalten ist, wird der Benutzername abgelehnt. Benutzer mit Moderatorenrechten sind von dieser Einschränkung nicht betroffen."
yourNameContainsProhibitedWords: "Dein Name enthält einen verbotenen Begriff"
yourNameContainsProhibitedWordsDescription: "Der Name enthält eine verbotene Zeichenfolge. Wende dich an deinen Serveradministrator, wenn du diesen Namen verwenden möchtest."
+thisContentsAreMarkedAsSigninRequiredByAuthor: "Logge dich ein, um weitere Inhalte von diesem Nutzer zu sehen."
+lockdown: "Sperren"
pleaseSelectAccount: "Bitte Konto auswählen"
availableRoles: "Verfügbare Rollen"
+federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren."
+federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren."
_accountSettings:
requireSigninToViewContents: "Anmeldung erfordern, um Inhalte anzuzeigen"
requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln."
+ requireSigninToViewContentsDescription2: "Der Inhalt wird nicht in URL-Vorschauen (OGP), eingebettet in Webseiten oder auf Servern, die keine Zitate unterstützen, angezeigt."
requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern."
makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar"
makeNotesHiddenBefore: "Frühere Notizen privat machen"
+ makeNotesHiddenBeforeDescription: ""
mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden."
+ notesOlderThanSpecifiedDateAndTime: "Notizen vor einem bestimmtem Datum und Uhrzeit"
_abuseUserReport:
forward: "Weiterleiten"
forwardDescription: "Leite die Meldung an einen entfernten Server als anonymes Systemkonto weiter."
+ resolve: "lösen"
accept: "Akzeptieren"
reject: "Ablehnen"
_delivery:
@@ -1381,6 +1431,7 @@ _serverSettings:
fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden."
fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen"
fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. "
+ openRegistrationWarning: "Das Aktivieren von Registrierungen ist riskant. Es wird empfohlen, sie nur dann zu aktivieren, wenn der Server ständig überwacht wird und im Falle eines Problems sofort reagiert werden kann."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern."
_accountMigration:
moveFrom: "Von einem anderen Konto zu diesem migrieren"
@@ -1842,6 +1893,7 @@ _channel:
notesCount: "{n} Notizen"
nameAndDescription: "Name und Beschreibung"
nameOnly: "Nur Name"
+ allowRenoteToExternal: "Renotes und Zitierungen außerhalb des Kanals erlauben"
_menuDisplay:
sideFull: "Seitlich"
sideIcon: "Seitlich (Icons)"
@@ -1930,6 +1982,7 @@ _sfx:
note: "Notizen"
noteMy: "Meine Notizen"
notification: "Benachrichtigungen"
+ reaction: "Auswählen einer Reaktion"
_soundSettings:
driveFile: "Audiodatei aus dem Drive verwenden"
driveFileWarn: "Wähle eine Audiodatei aus dem Drive"
@@ -2028,12 +2081,22 @@ _permissions:
"read:admin:server-info": "Serverinformationen anzeigen"
"read:admin:show-moderation-log": "Moderationsprotokoll einsehen"
"read:admin:show-user": "Private Benutzerinformationen einsehen"
+ "write:admin:roles": "Rollen verwalten"
+ "read:admin:roles": "Rollen anzeigen"
+ "write:admin:relays": "Relays verwalten"
+ "read:admin:relays": "Relays anzeigen"
"write:admin:invite-codes": "Einladungscodes verwalten"
"read:admin:invite-codes": "Einladungscodes anzeigen"
"write:admin:announcements": "Ankündigungen verwalten"
"read:admin:announcements": "Ankündigungen einsehen"
"write:admin:avatar-decorations": "Kann Avatar-Dekorationen verwalten"
"read:admin:avatar-decorations": "Avatar-Dekorationen ansehen"
+ "write:admin:account": "Benutzerkonten verwalten"
+ "read:admin:account": "Benutzerkonten anzeigen"
+ "write:admin:emoji": "Emojis verwalten"
+ "read:admin:emoji": "Emojis anzeigen"
+ "write:admin:queue": "Job-Warteschlange verwalten"
+ "read:admin:queue": "Job-Warteschlange anzeigen"
_auth:
shareAccessTitle: "Verteilung von App-Berechtigungen"
shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?"
@@ -2151,6 +2214,8 @@ _profile:
changeAvatar: "Profilbild ändern"
changeBanner: "Banner ändern"
verifiedLinkDescription: "Gibst du hier eine URL ein, die einen Link zu deinem Profile enthält, wird neben diesem Feld ein Icon zur Besitzbestätigung angezeigt."
+ avatarDecorationMax: "Du kannst bis zu {max} Dekorationen hinzufügen."
+ followedMessageDescription: "Du kannst eine kurze Nachricht festlegen, die dem Empfänger angezeigt wird, wenn er dir folgt."
_exportOrImport:
allNotes: "Alle Notizen"
favoritedNotes: "Als Favorit markierte Notizen"
@@ -2212,9 +2277,6 @@ _pages:
newPage: "Seite erstellen"
editPage: "Seite bearbeiten"
readPage: "Quelltextansicht"
- created: "Seite erfolgreich erstellt"
- updated: "Seite erfolgreich aktualisiert"
- deleted: "Seite erfolgreich gelöscht"
pageSetting: "Seiteneinstellungen"
nameAlreadyExists: "Die angegebene Seiten-URL existiert bereits"
invalidNameTitle: "Die angegebene Seiten-URL ist ungültig"
@@ -2283,6 +2345,7 @@ _notification:
reactedBySomeUsers: "{n} Benutzer haben eine Reaktion geschickt"
renotedBySomeUsers: "Renote von {n} Benutzern"
followedBySomeUsers: "Von {n} Benutzern gefolgt"
+ login: "Neue Anmeldung erfolgt"
_types:
all: "Alle"
note: "Neue Notizen"
@@ -2295,6 +2358,7 @@ _notification:
pollEnded: "Ende von Umfragen"
receiveFollowRequest: "Erhaltene Follow-Anfragen"
followRequestAccepted: "Akzeptierte Follow-Anfragen"
+ roleAssigned: "Rolle zugewiesen"
achievementEarned: "Errungenschaft freigeschaltet"
login: "Anmelden"
app: "Benachrichtigungen von Apps"
@@ -2346,6 +2410,7 @@ _webhookSettings:
createWebhook: "Webhook erstellen"
name: "Name"
secret: "Secret"
+ trigger: "Auslöser"
active: "Aktiviert"
_events:
follow: "Wenn du jemandem folgst"
@@ -2357,8 +2422,10 @@ _webhookSettings:
mention: "Wenn du erwähnt wirst"
_abuseReport:
_notificationRecipient:
+ createRecipient: "Meldungsempfänger hinzufügen"
_recipientType:
mail: "Email"
+ keywords: "Schlüsselwort"
_moderationLogTypes:
createRole: "Rolle erstellt"
deleteRole: "Rolle gelöscht"
@@ -2393,6 +2460,13 @@ _moderationLogTypes:
createAvatarDecoration: "Profilbilddekoration erstellt"
updateAvatarDecoration: "Profilbilddekoration aktualisiert"
deleteAvatarDecoration: "Profilbilddekoration gelöscht"
+ unsetUserAvatar: "Profilbild zurückgesetzt"
+ unsetUserBanner: "Profilbanner zurückgesetzt"
+ createSystemWebhook: "System-Webhook erstellt"
+ updateSystemWebhook: "System-Webhook aktualisiert"
+ deleteSystemWebhook: "System-Webhook gelöscht"
+ deletePage: "Seite gelöscht"
+ deleteGalleryPost: "Galeriebeitrag gelöscht"
_fileViewer:
title: "Dateiinformationen"
type: "Dateityp"
@@ -2442,6 +2516,10 @@ _externalResourceInstaller:
_themeInstallFailed:
title: "Das Farbschema konnte nicht installiert werden"
description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
+_hemisphere:
+ N: "Nördliche Erdhalbkugel"
+ S: "Südliche Erdhalbkugel"
+ caption: "Wird in einigen Client-Einstellungen zur Bestimmung der Jahreszeit verwendet."
_reversi:
blackOrWhite: "Schwarz/Weiß"
rules: "Regeln"
@@ -2449,17 +2527,26 @@ _reversi:
white: "Weiß"
total: "Gesamt"
_offlineScreen:
+ title: "Offline - keine Verbindung zum Server möglich"
header: "Verbindung zum Server nicht möglich"
_urlPreviewSetting:
title: "Einstellungen der URL-Vorschau"
enable: "URL-Vorschau aktivieren"
timeout: "Zeitüberschreitung beim Abrufen der Vorschau (ms)"
+ timeoutDescription: "Übersteigt die für die Vorschau benötigte Zeit diesen Wert, wird keine Vorschau generiert."
maximumContentLength: "Maximale Content-Length (Bytes)"
+ maximumContentLengthDescription: "Wenn die Content-Length diesen Wert überschreitet, wird keine Vorschau erzeugt."
+ requireContentLength: "Vorschau nur generieren, wenn Content-Length verfügbar ist"
+ requireContentLengthDescription: "Wenn der Server keine Content-Length zurückgibt, wird keine Vorschau erzeugt."
+ userAgent: "User-Agent"
_mediaControls:
playbackRate: "Wiedergabegeschwindigkeit"
_contextMenu:
title: "Kontextmenü"
app: "Anwendung"
+_gridComponent:
+ _error:
+ requiredValue: "Dieser Wert ist ein Pflichtfeld"
_embedCodeGen:
title: "Einbettungscode anpassen"
header: "Kopfzeile anzeigen"
@@ -2476,3 +2563,13 @@ _selfXssPrevention:
title: "„Füge in diesen Bereich etwas ein“ ist eine Betrugsmasche."
description1: "Wenn du hier etwas einfügst, könnte ein böswilliger Benutzer dein Konto übernehmen oder deine persönlichen Daten stehlen."
description3: "Weitere Informationen findest du hier. {link}"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "Kommunikation mit diesem Server nicht möglich"
+ description: "Möglicherweise wurde die Kommunikation mit diesem Server deaktiviert oder dieser Server ist blockiert.\nWende dich bitte an den Serveradministrator."
+ _uriInvalid:
+ title: "URI ist fehlerhaft"
+ description: "Es gibt ein Problem mit der von dir eingegebenen URI. Bitte prüfe, ob du Zeichen eingegeben hast, die in der URI nicht verwendet werden können."
+ _noSuchObject:
+ title: "Nicht gefunden"
+ description: "Die angeforderte Ressource konnte nicht gefunden werden, bitte überprüfe die URI erneut."
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 2f2d10c6b5..7a03669101 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -5,6 +5,7 @@ introMisskey: "Welcome! Misskey is an open source, decentralized microblogging s
poweredByMisskeyDescription: "{name} is one of the services powered by the open source platform <b>Misskey</b> (referred to as a \"Misskey instance\")."
monthAndDay: "{month}/{day}"
search: "Search"
+reset: "Reset"
notifications: "Notifications"
username: "Username"
password: "Password"
@@ -48,6 +49,7 @@ pin: "Pin to profile"
unpin: "Unpin from profile"
copyContent: "Copy contents"
copyLink: "Copy link"
+copyRemoteLink: "Copy remote link"
copyLinkRenote: "Copy renote link"
delete: "Delete"
deleteAndEdit: "Delete and edit"
@@ -684,11 +686,15 @@ smtpSecure: "Use implicit SSL/TLS for SMTP connections"
smtpSecureInfo: "Turn this off when using STARTTLS"
testEmail: "Test email delivery"
wordMute: "Word mute"
+wordMuteDescription: "Minimize notes that contain the specified word or phrase. Minimized notes can be displayed by clicking on them."
hardWordMute: "Hard word mute"
+showMutedWord: "Show muted words"
+hardWordMuteDescription: "Hide notes that contain the specified word or phrase. Unlike word mute, the note will be completely hidden from view."
regexpError: "Regular Expression error"
regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:"
instanceMute: "Instance Mutes"
userSaysSomething: "{name} said something"
+userSaysSomethingAbout: "{name} said something about \"{word}\""
makeActive: "Activate"
display: "Display"
copy: "Copy"
@@ -1300,6 +1306,9 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require log
lockdown: "Lockdown"
pleaseSelectAccount: "Select an account"
availableRoles: "Available roles"
+acknowledgeNotesAndEnable: "Turn on after understanding the precautions."
+federationSpecified: "This server is operated in an allowlist federation. Interacting with servers other than those designated by the administrator is not allowed."
+federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers."
_accountSettings:
requireSigninToViewContents: "Require sign-in to view contents"
requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information."
@@ -1456,9 +1465,9 @@ _serverSettings:
reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase."
inquiryUrl: "Inquiry URL"
inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information."
+ openRegistration: "Make the account creation open"
+ openRegistrationWarning: "Opening registration carries risks. It is recommended to only enable it if you have a system in place to continuously monitor the server and respond immediately in case of any issues."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "If no moderator activity is detected for a while, this setting will be automatically turned off to prevent spam."
- openRegistration: "Open account creation"
- openRegistrationWarning: "Opening up registration is risky, so we recommend turning it on only if you are able to constantly monitor your server and respond immediately if any problems arise."
_accountMigration:
moveFrom: "Migrate another account to this one"
moveFromSub: "Create alias to another account"
@@ -2356,9 +2365,6 @@ _pages:
newPage: "Create a new Page"
editPage: "Edit this Page"
readPage: "Viewing this Page's source"
- created: "Page successfully created"
- updated: "Page successfully edited"
- deleted: "Page successfully deleted"
pageSetting: "Page settings"
nameAlreadyExists: "The specified Page URL already exists"
invalidNameTitle: "The specified Page URL is invalid"
@@ -2720,6 +2726,66 @@ _contextMenu:
app: "Application"
appWithShift: "Application with shift key"
native: "Native"
+_gridComponent:
+ _error:
+ requiredValue: "This value is required"
+ columnTypeNotSupport: "Validation with regular expression is supported only for type:text columns."
+ patternNotMatch: "This value doesn't match the pattern in {pattern}"
+ notUnique: "This value must be unique"
+_roleSelectDialog:
+ notSelected: "Not selected"
+_customEmojisManager:
+ _gridCommon:
+ copySelectionRows: "Copy selected rows"
+ copySelectionRanges: "Copy selected ranges"
+ deleteSelectionRows: "Delete selected rows"
+ deleteSelectionRanges: "Delete rows in the selection"
+ searchSettings: "Search settings"
+ searchSettingCaption: "Set detailed search criteria."
+ searchLimit: "Limit results"
+ sortOrder: "Sort order"
+ registrationLogs: "Registration log"
+ registrationLogsCaption: "Logs will be displayed when updating or deleting Emojis. They will disappear after updating or deleting them, moving to a new page, or reloading."
+ alertEmojisRegisterFailedDescription: "Failed to update or delete Emojis. Please check the registration log for details."
+ _logs:
+ showSuccessLogSwitch: "Show success log"
+ failureLogNothing: "There is no failure log."
+ logNothing: "There is no log."
+ _remote:
+ selectionRowDetail: "Selected row's detail"
+ importSelectionRows: "Import selected rows"
+ importSelectionRangesRows: "Import rows in the selection"
+ importEmojisButton: "Import checked Emojis"
+ confirmImportEmojisTitle: "Import Emojis"
+ confirmImportEmojisDescription: "Import {count} Emoji(s) received from the remote server. Please pay close attention to the license of the Emoji. Are you sure to continue?"
+ _local:
+ tabTitleList: "List of registered Emojis"
+ tabTitleRegister: "Emoji registration"
+ _list:
+ emojisNothing: "There are no registered Emojis."
+ markAsDeleteTargetRows: "Mark selected rows as a target to delete"
+ markAsDeleteTargetRanges: "Mark rows in the selection as a target to delete"
+ alertUpdateEmojisNothingDescription: "There are no updated Emojis."
+ alertDeleteEmojisNothingDescription: "There are no Emojis to be deleted."
+ confirmMovePage: "Do you want to move the page?"
+ confirmChangeView: "Do you want to change the view?"
+ confirmUpdateEmojisDescription: "Update {count} Emoji(s). Are you sure to continue?"
+ confirmDeleteEmojisDescription: "Delete checked {count} Emoji(s). Are you sure to continue?"
+ confirmResetDescription: "This will reset any changes you have made so far"
+ confirmMovePageDesciption: "Changes have been made to the Emojis on this page.\nIf you leave the page without saving, all changes made on this page will be discarded."
+ dialogSelectRoleTitle: "Search by roll set in Emojis"
+ _register:
+ uploadSettingTitle: "Upload settings"
+ uploadSettingDescription: "On this screen, you can configure the behavior when uploading Emojis."
+ directoryToCategoryLabel: "Enter the directory name in the \"category\" field"
+ directoryToCategoryCaption: "When you drag and drop a directory, enter the directory name in the \"category\" field."
+ emojiInputAreaCaption: "Select the Emojis you wish to register using one of the methods."
+ emojiInputAreaList1: "Drag and drop image files or a directory into this frame"
+ emojiInputAreaList2: "Click this link to select from your computer"
+ emojiInputAreaList3: "Click this link to select from the drive"
+ confirmRegisterEmojisDescription: "Register the Emojis from the list as new custom Emojis. Are you sure to continue? (To avoid overload, only {count} Emoji(s) can be registered in a single operation)"
+ confirmClearEmojisDescription: "Discard the edits and clear the Emojis from the list. Are you sure to continue?"
+ confirmUploadEmojisDescription: "Upload the dragged and dropped {count} file(s) to the drive. Are you sure to continue?"
_embedCodeGen:
title: "Customize embed code"
header: "Show header"
@@ -2743,4 +2809,34 @@ _selfXssPrevention:
_followRequest:
recieved: "Received"
sent: "Sent"
-acknowledgeNotesAndEnable: "Be sure you have understood the warnings before turning this on"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "Unable to communicate with this server"
+ description: "Communication with this server may have been disabled or this server may be blocked.\nPlease contact the server administrator."
+ _uriInvalid:
+ title: "URI is invalid"
+ description: "There is a problem with the URI you entered. Please check if you entered characters that cannot be used in the URI."
+ _requestFailed:
+ title: "Request failed"
+ description: "Communication with this server failed. The server may be down. Also, please make sure that you have not entered an invalid or nonexistent URI."
+ _responseInvalid:
+ title: "Response is invalid"
+ description: "It could communicate with this server, but the data obtained was incorrect."
+ _responseInvalidIdHostNotMatch:
+ description: "The domain of the entered URI differs from the domain of the final obtained URI. If you are looking up remote content through a third-party server, please look up again using a URI that can be obtained from the origin server."
+ _noSuchObject:
+ title: "Not found"
+ description: "The requested resource was not found, please recheck the URI."
+_captcha:
+ verify: "Please verify the CAPTCHA"
+ testSiteKeyMessage: "You can check the preview by entering the test values for the site and secret keys.\nPlease see the following page for details."
+ _error:
+ _requestFailed:
+ title: "Failed to request CAPTCHA"
+ text: "Please run it after a while or check the settings again."
+ _verificationFailed:
+ title: "Failed to validate CAPTCHA"
+ text: "Please check again if the settings are correct."
+ _unknown:
+ title: "CAPTCHA error"
+ text: "An unexpected error occurred."
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index a4ec114b15..0b1411d84b 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -5,11 +5,13 @@ introMisskey: "¡Bienvenido/a! Misskey es un servicio de microblogging descentra
poweredByMisskeyDescription: "{name} es uno de los servicios (también llamado instancia) que usa la plataforma de código abierto <b>Misskey</b>"
monthAndDay: "{day}/{month}"
search: "Buscar"
+reset: "Reiniciar"
notifications: "Notificaciones"
username: "Nombre de usuario"
password: "Contraseña"
initialPasswordForSetup: "Contraseña para iniciar la inicialización"
initialPasswordIsIncorrect: "La contraseña para iniciar la configuración inicial es incorrecta."
+initialPasswordForSetupDescription: "Si ha instalado Misskey usted mismo, utilice la contraseña introducida en el archivo de configuración.\nSi utiliza un servicio de alojamiento de Misskey o similar, utilice la contraseña proporcionada.\nSi no ha establecido una contraseña, déjela en blanco para continuar."
forgotPassword: "Olvidé mi contraseña"
fetchingAsApObject: "Buscando en el fediverso"
ok: "OK"
@@ -47,6 +49,7 @@ pin: "Fijar al perfil"
unpin: "Desfijar"
copyContent: "Copiar contenido"
copyLink: "Copiar enlace"
+copyRemoteLink: "Copiar enlace remoto"
copyLinkRenote: "Copiar enlace de renota"
delete: "Borrar"
deleteAndEdit: "Borrar y editar"
@@ -198,6 +201,7 @@ followConfirm: "¿Desea seguir a {name}?"
proxyAccount: "Cuenta proxy"
proxyAccountDescription: "Una cuenta proxy es una cuenta que actúa como un seguidor remoto de un usuario bajo ciertas condiciones. Por ejemplo, cuando un usuario añade un usuario remoto a una lista, si ningún usuario local sigue al usuario agregado a la lista, la instancia no puede obtener su actividad. Así que la cuenta proxy sigue al usuario añadido a la lista"
host: "Host"
+selectSelf: "Elígete a ti mismo"
selectUser: "Elegir usuario"
recipient: "Recipiente"
annotation: "Anotación"
@@ -213,6 +217,7 @@ perDay: "por día"
stopActivityDelivery: "Dejar de enviar actividades"
blockThisInstance: "Bloquear instancia"
silenceThisInstance: "Silenciar esta instancia"
+mediaSilenceThisInstance: "Silencia la Multimedia(Imágenes,videos...) para este servidor"
operations: "Operaciones"
software: "Software"
version: "Versión"
@@ -234,6 +239,10 @@ blockedInstances: "Instancias bloqueadas"
blockedInstancesDescription: "Seleccione los hosts de las instancias que desea bloquear, separadas por una linea nueva. Las instancias bloqueadas no podrán comunicarse con esta instancia."
silencedInstances: "Instancias silenciadas"
silencedInstancesDescription: "Listar los hostname de las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas."
+mediaSilencedInstances: "Servidores silenciados (Multimedia)"
+mediaSilencedInstancesDescription: "Listar las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas."
+federationAllowedHosts: "Servidores federados"
+federationAllowedHostsDescription: "Establezca los nombres de los servidores que pueden federarse, separados por una nueva línea."
muteAndBlock: "Silenciar y bloquear"
mutedUsers: "Usuarios silenciados"
blockedUsers: "Usuarios bloqueados"
@@ -324,6 +333,7 @@ selectFile: "Elegir archivo"
selectFiles: "Elegir archivos"
selectFolder: "Seleccione una carpeta"
selectFolders: "Seleccione carpetas"
+fileNotSelected: "Archivo no seleccionado."
renameFile: "Renombrar archivo"
folderName: "Nombre de la carpeta"
createFolder: "Crear carpeta"
@@ -331,6 +341,7 @@ renameFolder: "Renombrar carpeta"
deleteFolder: "Borrar carpeta"
folder: "Carpeta"
addFile: "Agregar archivo"
+showFile: "Examinar archivos"
emptyDrive: "El drive está vacío"
emptyFolder: "La carpeta está vacía"
unableToDelete: "No se puede borrar"
@@ -444,6 +455,7 @@ totpDescription: "Ingresa una contaseña de un sólo uso usando la aplicación a
moderator: "Moderador"
moderation: "Moderación"
moderationNote: "Nota de moderación"
+moderationNoteDescription: "Puedes rellenar notas que solo se comparten entre moderadores."
addModerationNote: "Añadir nota de moderación"
moderationLogs: "Log de moderación"
nUsersMentioned: "{n} usuarios mencionados"
@@ -478,10 +490,12 @@ retype: "Ingrese de nuevo"
noteOf: "Notas de {user}"
quoteAttached: "Cita añadida"
quoteQuestion: "¿Quiere añadir una cita?"
+attachAsFileQuestion: "El texto del portapapeles es demasiado grande ¿Desea adjuntarlo como archivo de texto?"
noMessagesYet: "Aún no hay chat"
newMessageExists: "Tienes un mensaje nuevo"
onlyOneFileCanBeAttached: "Solo se puede añadir un archivo al mensaje"
signinRequired: "Iniciar sesión"
+signinOrContinueOnRemote: "Para continuar, tendrá que ir a su servidor o registrarse e iniciar sesión en este servidor"
invitations: "Invitar"
invitationCode: "Código de invitación"
checking: "Comprobando"
@@ -505,6 +519,8 @@ emojiStyle: "Estilo de emoji"
native: "Nativo"
menuStyle: "Diseño del menú"
style: "Diseño"
+drawer: "Cajón de Aplicaciones"
+popup: "Ventana emergente"
showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor"
showReactionsCount: "Mostrar el número de reacciones en las notas"
noHistory: "No hay datos en el historial"
@@ -572,6 +588,7 @@ masterVolume: "Volumen principal"
notUseSound: "Sin sonido"
useSoundOnlyWhenActive: "Sonar solo cuando Misskey esté activo"
details: "Detalles"
+renoteDetails: "Detalles(Renota)"
chooseEmoji: "Elije un emoji"
unableToProcess: "La operación no se puede llevar a cabo"
recentUsed: "Usado recientemente"
@@ -587,6 +604,7 @@ ascendingOrder: "Ascendente"
descendingOrder: "Descendente"
scratchpad: "Scratch pad"
scratchpadDescription: "Scratchpad proporciona un entorno experimental para AiScript. Puede escribir, ejecutar y verificar los resultados que interactúan con Misskey."
+uiInspector: "Inspector de UI"
output: "Salida"
script: "Script"
disablePagesScript: "Deshabilitar AiScript en Páginas"
@@ -667,7 +685,10 @@ smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP"
smtpSecureInfo: "Apagar cuando se use STARTTLS"
testEmail: "Prueba de envío"
wordMute: "Silenciar palabras"
+wordMuteDescription: "Minimiza las notas que contienen la palabra o frase especificada. Las notas minimizadas pueden visualizarse haciendo clic sobre ellas."
hardWordMute: "Filtro de palabra fuerte"
+showMutedWord: "Mostrar palabras silenciadas."
+hardWordMuteDescription: "Oculta las notas que contienen la palabra o frase especificada. A diferencia de Silenciar palabra, la nota quedará completamente oculta a la vista."
regexpError: "Error de la expresión regular"
regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line} de las palabras muteadas {tab}"
instanceMute: "Instancias silenciadas"
@@ -1117,6 +1138,8 @@ preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generati
preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podría lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada."
options: "Opción"
specifyUser: "Especificar usuario"
+lookupConfirm: "¿Quiere informarse?"
+specifyHost: "Especificar Host"
failedToPreviewUrl: "No se pudo generar la vista previa"
update: "Actualizar"
rolesThatCanBeUsedThisEmojiAsReaction: "Roles que pueden usar este emoji como reacción"
@@ -1251,6 +1274,11 @@ tryAgain: "Por favor , inténtalo de nuevo"
performance: "Rendimiento"
unknownWebAuthnKey: "Esto no se ha registrado llave maestra."
messageToFollower: "Mensaje a seguidores"
+federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador."
+federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores"
+_accountSettings:
+ requireSigninToViewContents: "Se requiere iniciar sesión para ver el contenido"
+ requireSigninToViewContentsDescription1: "Requiere iniciar sesión para ver todas las notas y otros contenidos que hayas creado. Se espera que esto evite que los rastreadores recopilen información."
_abuseUserReport:
accept: "Acepte"
reject: "repudio"
@@ -2266,9 +2294,6 @@ _pages:
newPage: "Crear página"
editPage: "Editar página"
readPage: "Viendo la fuente"
- created: "La página fue creada"
- updated: "La página fue actualizada"
- deleted: "La página borrada"
pageSetting: "Configurar página"
nameAlreadyExists: "La URL de la página especificada ya existe"
invalidNameTitle: "URL inválida"
@@ -2535,3 +2560,6 @@ _mediaControls:
pip: "Picture in Picture"
playbackRate: "Velocidad de reproducción"
loop: "Reproducción en bucle"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "No se encuentra"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index b105a86b5e..ccfd462a76 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -2118,9 +2118,6 @@ _pages:
newPage: "Créer une page"
editPage: "Modifier une page"
readPage: "Affichage de la source en cours"
- created: "La page a été créée !"
- updated: "La page a été mise à jour !"
- deleted: "La page a été supprimée"
pageSetting: "Paramètres de la Page"
nameAlreadyExists: "L'URL de page spécifiée existe déjà"
invalidNameTitle: "L'URL de page spécifiée n’est pas valide"
@@ -2364,3 +2361,6 @@ _mediaControls:
_embedCodeGen:
title: "Personnaliser le code d'intégration"
generateCode: "Générer le code d'intégration"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Non trouvé"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index fe3f207618..7be56b1494 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -2285,9 +2285,6 @@ _pages:
newPage: "Buat halaman baru"
editPage: "Sunting halaman"
readPage: "Lihat sumber kode aktif"
- created: "Halaman berhasil dibuat"
- updated: "Halaman berhasil diperbaharui!"
- deleted: "Halaman telah dihapus"
pageSetting: "Pengaturan Halaman"
nameAlreadyExists: "URL Halaman yang ditentukan sudah ada"
invalidNameTitle: "URL Halaman yang ditentukan tidak valid"
@@ -2610,3 +2607,6 @@ _mediaControls:
pip: "Gambar dalam Gambar"
playbackRate: "Kecepatan Pemutaran"
loop: "Ulangi Pemutaran"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Tidak dapat ditemukan"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index bcb78e4ee1..27c8a36842 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -37,6 +37,10 @@ export interface Locale extends ILocale {
*/
"search": string;
/**
+ * リセット
+ */
+ "reset": string;
+ /**
* 通知
*/
"notifications": string;
@@ -211,6 +215,10 @@ export interface Locale extends ILocale {
*/
"copyLink": string;
/**
+ * リモートのリンクをコピー
+ */
+ "copyRemoteLink": string;
+ /**
* Copy boost link
*/
"copyLinkRenote": string;
@@ -1395,7 +1403,7 @@ export interface Locale extends ILocale {
*/
"inputNewFileName": string;
/**
- * 新しいキャプションを入力してください
+ * Enter new alt text
*/
"inputNewDescription": string;
/**
@@ -2595,11 +2603,11 @@ export interface Locale extends ILocale {
*/
"description": string;
/**
- * キャプションを付ける
+ * Add alt text
*/
"describeFile": string;
/**
- * キャプションを入力
+ * Enter alt text
*/
"enterFileDescription": string;
/**
@@ -2755,10 +2763,22 @@ export interface Locale extends ILocale {
*/
"wordMute": string;
/**
+ * 指定した語句を含むノートを最小化します。最小化されたノートをクリックすることで表示することができます。
+ */
+ "wordMuteDescription": string;
+ /**
* ハードワードミュート
*/
"hardWordMute": string;
/**
+ * ミュートされたワードを表示
+ */
+ "showMutedWord": string;
+ /**
+ * 指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。
+ */
+ "hardWordMuteDescription": string;
+ /**
* 正規表現エラー
*/
"regexpError": string;
@@ -2775,6 +2795,10 @@ export interface Locale extends ILocale {
*/
"userSaysSomething": ParameterizedString<"name">;
/**
+ * {name}が「{word}」について何かを言いました
+ */
+ "userSaysSomethingAbout": ParameterizedString<"name" | "word">;
+ /**
* アクティブにする
*/
"makeActive": string;
@@ -4060,7 +4084,7 @@ export interface Locale extends ILocale {
*/
"windowRestore": string;
/**
- * キャプション
+ * Alt text
*/
"caption": string;
/**
@@ -4172,7 +4196,7 @@ export interface Locale extends ILocale {
*/
"invalidParamError": string;
/**
- * リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。
+ * リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる・許可されていない文字を入力している等の可能性もあります。
*/
"invalidParamErrorDescription": string;
/**
@@ -5223,6 +5247,14 @@ export interface Locale extends ILocale {
* 注意事項を理解した上でオンにします。
*/
"acknowledgeNotesAndEnable": string;
+ /**
+ * このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。
+ */
+ "federationSpecified": string;
+ /**
+ * このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。
+ */
+ "federationDisabled": string;
"_accountSettings": {
/**
* コンテンツの表示にログインを必須にする
@@ -8507,6 +8539,38 @@ export interface Locale extends ILocale {
*/
"write:report-abuse": string;
/**
+ * Approve new users
+ */
+ "write:admin:approve-user": string;
+ /**
+ * Decline new users
+ */
+ "write:admin:decline-user": string;
+ /**
+ * Mark users as NSFW
+ */
+ "write:admin:nsfw-user": string;
+ /**
+ * Mark users an not NSFW
+ */
+ "write:admin:unnsfw-user": string;
+ /**
+ * Apply mandatory CW on users
+ */
+ "write:admin:cw-user": string;
+ /**
+ * Silence users
+ */
+ "write:admin:silence-user": string;
+ /**
+ * Un-silence users
+ */
+ "write:admin:unsilence-user": string;
+ /**
+ * Allow/Prohibit quote posts from a user
+ */
+ "write:admin:reject-quotes": string;
+ /**
* View your list of scheduled notes
*/
"read:notes-schedule": string;
@@ -9020,6 +9084,10 @@ export interface Locale extends ILocale {
* Remove background
*/
"removeBackground": string;
+ /**
+ * ListenBrainz username
+ */
+ "listenbrainz": string;
};
"_exportOrImport": {
/**
@@ -9261,18 +9329,6 @@ export interface Locale extends ILocale {
*/
"readPage": string;
/**
- * ページを作成しました
- */
- "created": string;
- /**
- * ページを更新しました
- */
- "updated": string;
- /**
- * ページを削除しました
- */
- "deleted": string;
- /**
* ページ設定
*/
"pageSetting": string;
@@ -10167,6 +10223,14 @@ export interface Locale extends ILocale {
*/
"approve": string;
/**
+ * Declined
+ */
+ "decline": string;
+ /**
+ * Set content warning for user
+ */
+ "setMandatoryCW": string;
+ /**
* Set remote instance as NSFW
*/
"setRemoteInstanceNSFW": string;
@@ -10182,6 +10246,74 @@ export interface Locale extends ILocale {
* Accepted reports from remote instance
*/
"acceptRemoteInstanceReports": string;
+ /**
+ * Blocked/Stripped quote posts from user
+ */
+ "rejectQuotesUser": string;
+ /**
+ * Allowed quote posts from user
+ */
+ "allowQuotesUser": string;
+ /**
+ * Cleared a user's drive files
+ */
+ "clearUserFiles": string;
+ /**
+ * Marked user as NSFW
+ */
+ "nsfwUser": string;
+ /**
+ * Un-marked user as NSFW
+ */
+ "unNsfwUser": string;
+ /**
+ * Silenced user
+ */
+ "silenceUser": string;
+ /**
+ * Un-silenced user
+ */
+ "unSilenceUser": string;
+ /**
+ * Created an account
+ */
+ "createAccount": string;
+ /**
+ * Cleared remote drive files
+ */
+ "clearRemoteFiles": string;
+ /**
+ * Cleared owner-less drive files
+ */
+ "clearOwnerlessFiles": string;
+ /**
+ * Updated custom emojis
+ */
+ "updateCustomEmojis": string;
+ /**
+ * Imported custom emojis
+ */
+ "importCustomEmojis": string;
+ /**
+ * Cleared an instance's drive files
+ */
+ "clearInstanceFiles": string;
+ /**
+ * Severed follow relations with an instance
+ */
+ "severFollowRelations": string;
+ /**
+ * Created a note promo
+ */
+ "createPromo": string;
+ /**
+ * Added a relay
+ */
+ "addRelay": string;
+ /**
+ * Removed a relay
+ */
+ "removeRelay": string;
};
"_fileViewer": {
/**
@@ -10675,6 +10807,227 @@ export interface Locale extends ILocale {
*/
"native": string;
};
+ "_gridComponent": {
+ "_error": {
+ /**
+ * この値は必須項目です
+ */
+ "requiredValue": string;
+ /**
+ * 正規表現によるバリデーションはtype:textのカラムのみサポートします。
+ */
+ "columnTypeNotSupport": string;
+ /**
+ * この値は{pattern}のパターンに一致しません
+ */
+ "patternNotMatch": ParameterizedString<"pattern">;
+ /**
+ * この値は一意である必要があります
+ */
+ "notUnique": string;
+ };
+ };
+ "_roleSelectDialog": {
+ /**
+ * 選択されていません
+ */
+ "notSelected": string;
+ };
+ "_customEmojisManager": {
+ "_gridCommon": {
+ /**
+ * 選択行をコピー
+ */
+ "copySelectionRows": string;
+ /**
+ * 選択範囲をコピー
+ */
+ "copySelectionRanges": string;
+ /**
+ * 選択行を削除
+ */
+ "deleteSelectionRows": string;
+ /**
+ * 選択範囲の値をクリア
+ */
+ "deleteSelectionRanges": string;
+ /**
+ * 検索設定
+ */
+ "searchSettings": string;
+ /**
+ * 検索条件を詳細に設定します。
+ */
+ "searchSettingCaption": string;
+ /**
+ * 表示件数
+ */
+ "searchLimit": string;
+ /**
+ * 並び順
+ */
+ "sortOrder": string;
+ /**
+ * 登録ログ
+ */
+ "registrationLogs": string;
+ /**
+ * 絵文字更新・削除時のログが表示されます。更新・削除操作を行ったり、ページを遷移・リロードすると消えます。
+ */
+ "registrationLogsCaption": string;
+ /**
+ * 絵文字の更新・削除に失敗しました。詳細は登録ログをご確認ください。
+ */
+ "alertEmojisRegisterFailedDescription": string;
+ };
+ "_logs": {
+ /**
+ * 成功ログを表示
+ */
+ "showSuccessLogSwitch": string;
+ /**
+ * 失敗ログはありません。
+ */
+ "failureLogNothing": string;
+ /**
+ * ログはありません。
+ */
+ "logNothing": string;
+ };
+ "_remote": {
+ /**
+ * 選択行の詳細
+ */
+ "selectionRowDetail": string;
+ /**
+ * 選択行をインポート
+ */
+ "importSelectionRows": string;
+ /**
+ * 選択範囲の行をインポート
+ */
+ "importSelectionRangesRows": string;
+ /**
+ * チェックされた絵文字をインポート
+ */
+ "importEmojisButton": string;
+ /**
+ * 絵文字のインポート
+ */
+ "confirmImportEmojisTitle": string;
+ /**
+ * リモートから受信した{count}個の絵文字のインポートを行います。絵文字のライセンスに十分な注意を払ってください。実行しますか?
+ */
+ "confirmImportEmojisDescription": ParameterizedString<"count">;
+ };
+ "_local": {
+ /**
+ * 登録済み絵文字一覧
+ */
+ "tabTitleList": string;
+ /**
+ * 絵文字の登録
+ */
+ "tabTitleRegister": string;
+ "_list": {
+ /**
+ * 登録された絵文字はありません。
+ */
+ "emojisNothing": string;
+ /**
+ * 選択行を削除対象にする
+ */
+ "markAsDeleteTargetRows": string;
+ /**
+ * 選択範囲の行を削除対象にする
+ */
+ "markAsDeleteTargetRanges": string;
+ /**
+ * 変更された絵文字はありません。
+ */
+ "alertUpdateEmojisNothingDescription": string;
+ /**
+ * 削除対象の絵文字はありません。
+ */
+ "alertDeleteEmojisNothingDescription": string;
+ /**
+ * ページを移動しますか?
+ */
+ "confirmMovePage": string;
+ /**
+ * 表示を変更しますか?
+ */
+ "confirmChangeView": string;
+ /**
+ * {count}個の絵文字を更新します。実行しますか?
+ */
+ "confirmUpdateEmojisDescription": ParameterizedString<"count">;
+ /**
+ * チェックがつけられた{count}個の絵文字を削除します。実行しますか?
+ */
+ "confirmDeleteEmojisDescription": ParameterizedString<"count">;
+ /**
+ * 今までに加えた変更がすべてリセットされます。
+ */
+ "confirmResetDescription": string;
+ /**
+ * このページの絵文字に変更が加えられています。
+ * 保存せずにこのままページを移動すると、このページで加えた変更はすべて破棄されます。
+ */
+ "confirmMovePageDesciption": string;
+ /**
+ * 絵文字に設定されたロールで検索
+ */
+ "dialogSelectRoleTitle": string;
+ };
+ "_register": {
+ /**
+ * アップロード設定
+ */
+ "uploadSettingTitle": string;
+ /**
+ * この画面で絵文字アップロードを行う際の動作を設定できます。
+ */
+ "uploadSettingDescription": string;
+ /**
+ * ディレクトリ名を"category"に入力する
+ */
+ "directoryToCategoryLabel": string;
+ /**
+ * ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を"category"に入力します。
+ */
+ "directoryToCategoryCaption": string;
+ /**
+ * いずれかの方法で登録する絵文字を選択してください。
+ */
+ "emojiInputAreaCaption": string;
+ /**
+ * この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ
+ */
+ "emojiInputAreaList1": string;
+ /**
+ * このリンクをクリックしてPCから選択する
+ */
+ "emojiInputAreaList2": string;
+ /**
+ * このリンクをクリックしてドライブから選択する
+ */
+ "emojiInputAreaList3": string;
+ /**
+ * リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)
+ */
+ "confirmRegisterEmojisDescription": ParameterizedString<"count">;
+ /**
+ * 編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?
+ */
+ "confirmClearEmojisDescription": string;
+ /**
+ * ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?
+ */
+ "confirmUploadEmojisDescription": ParameterizedString<"count">;
+ };
+ };
+ };
"_embedCodeGen": {
/**
* 埋め込みコードをカスタマイズ
@@ -10761,6 +11114,108 @@ export interface Locale extends ILocale {
*/
"sent": string;
};
+ "_remoteLookupErrors": {
+ "_federationNotAllowed": {
+ /**
+ * このサーバーとは通信できません
+ */
+ "title": string;
+ /**
+ * このサーバーとの通信が無効化されているか、このサーバーをブロックしている・ブロックされている可能性があります。
+ * サーバー管理者にお問い合わせください。
+ */
+ "description": string;
+ };
+ "_uriInvalid": {
+ /**
+ * URIが不正です
+ */
+ "title": string;
+ /**
+ * 入力されたURIに問題があります。URIに使用できない文字を入力していないか確認してください。
+ */
+ "description": string;
+ };
+ "_requestFailed": {
+ /**
+ * リクエストに失敗しました
+ */
+ "title": string;
+ /**
+ * このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。
+ */
+ "description": string;
+ };
+ "_responseInvalid": {
+ /**
+ * レスポンスが不正です
+ */
+ "title": string;
+ /**
+ * このサーバーと通信することはできましたが、得られたデータが不正なものでした。
+ */
+ "description": string;
+ };
+ "_responseInvalidIdHostNotMatch": {
+ /**
+ * 入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。
+ */
+ "description": string;
+ };
+ "_noSuchObject": {
+ /**
+ * 見つかりません
+ */
+ "title": string;
+ /**
+ * 要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。
+ */
+ "description": string;
+ };
+ };
+ "_captcha": {
+ /**
+ * CAPTCHAを通過してください
+ */
+ "verify": string;
+ /**
+ * サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できます。
+ * 詳細は下記ページをご確認ください。
+ */
+ "testSiteKeyMessage": string;
+ "_error": {
+ "_requestFailed": {
+ /**
+ * CAPTCHAのリクエストに失敗しました
+ */
+ "title": string;
+ /**
+ * しばらく後に実行するか、設定をもう一度ご確認ください。
+ */
+ "text": string;
+ };
+ "_verificationFailed": {
+ /**
+ * CAPTCHAの検証に失敗しました
+ */
+ "title": string;
+ /**
+ * 設定が正しいかどうかもう一度確認ください。
+ */
+ "text": string;
+ };
+ "_unknown": {
+ /**
+ * CAPTCHAエラー
+ */
+ "title": string;
+ /**
+ * 想定外のエラーが発生しました。
+ */
+ "text": string;
+ };
+ };
+ };
/**
* Approvals
*/
@@ -10806,6 +11261,10 @@ export interface Locale extends ILocale {
*/
"date": string;
/**
+ * Boost (hold Shift for visibility menu)
+ */
+ "renoteShift": string;
+ /**
* Quoted.
*/
"quoted": string;
@@ -10854,6 +11313,26 @@ export interface Locale extends ILocale {
*/
"rejectReports": string;
/**
+ * Strip quote posts from this instance
+ */
+ "rejectQuotesInstance": string;
+ /**
+ * Strip quote posts from this user
+ */
+ "rejectQuotesRemoteUser": string;
+ /**
+ * Block quote posts from this user
+ */
+ "rejectQuotesLocalUser": string;
+ /**
+ * Are you sure you wish to strip quote posts?
+ */
+ "rejectQuotesConfirm": string;
+ /**
+ * Are you sure you wish to allow quote posts?
+ */
+ "allowQuotesConfirm": string;
+ /**
* This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s).
*/
"blockedByBase": string;
@@ -11182,6 +11661,18 @@ export interface Locale extends ILocale {
* Flash
*/
"flash": string;
+ /**
+ * Files removed
+ */
+ "filesRemoved": string;
+ /**
+ * File imported
+ */
+ "fileImported": string;
+ /**
+ * Failed to load note
+ */
+ "cannotLoadNote": string;
"_flash": {
/**
* Flash Content Hidden
@@ -11526,6 +12017,14 @@ export interface Locale extends ILocale {
*/
"backgroundDescription": string;
/**
+ * Border
+ */
+ "border": string;
+ /**
+ * Draw a border around the content.
+ */
+ "borderDescription": string;
+ /**
* Plain
*/
"plain": string;
@@ -11610,6 +12109,116 @@ export interface Locale extends ILocale {
* Scheduled Notes
*/
"scheduledNotes": string;
+ /**
+ * Custom robots.txt
+ */
+ "robotsTxt": string;
+ /**
+ * Adding entries here will override the default robots.txt packaged with Sharkey.
+ */
+ "robotsTxtDescription": string;
+ /**
+ * Default content warning for new posts
+ */
+ "defaultCW": string;
+ /**
+ * The value here will be auto-filled as the content warning for all new posts and replies.
+ */
+ "defaultCWDescription": string;
+ /**
+ * Automatic CW priority
+ */
+ "defaultCWPriority": string;
+ /**
+ * Select preferred action when default CW and keep CW settings are both enabled at the same time.
+ */
+ "defaultCWPriorityDescription": string;
+ "_defaultCWPriority": {
+ /**
+ * Use Default (use the default CW, ignoring the inherited CW)
+ */
+ "default": string;
+ /**
+ * Use Parent (use the inherited CW, ignoring the default CW)
+ */
+ "parent": string;
+ /**
+ * Use Default, then Parent (use the default CW, and append the inherited CW)
+ */
+ "defaultParent": string;
+ /**
+ * Use Parent, then Default (use the inherited CW, and append the default CW)
+ */
+ "parentDefault": string;
+ };
+ /**
+ * Generate Keys
+ */
+ "genKeys": string;
+ "_genKeysDialog": {
+ /**
+ * Are you sure that you want to generate new keys? This will stop push notifications for all users who have already enabled them.
+ */
+ "text": string;
+ /**
+ * Generate new keys
+ */
+ "title": string;
+ };
+ "_noteSearch": {
+ /**
+ * Sort by newest to oldest
+ */
+ "newestToOldest": string;
+ /**
+ * File Type
+ */
+ "fileType": string;
+ "_fileType": {
+ /**
+ * None
+ */
+ "none": string;
+ /**
+ * Images
+ */
+ "image": string;
+ /**
+ * Videos
+ */
+ "video": string;
+ /**
+ * Audio
+ */
+ "audio": string;
+ /**
+ * Module
+ */
+ "module": string;
+ /**
+ * Flash
+ */
+ "flash": string;
+ };
+ };
+ /**
+ * ID
+ */
+ "id": string;
+ /**
+ * Force content warning
+ */
+ "mandatoryCW": string;
+ /**
+ * Applies a content warning to all posts created by this user. If the post already has a CW, then this is appended to the end.
+ */
+ "mandatoryCWDescription": string;
+ "_processErrors": {
+ /**
+ * Unable to process quote. This post may be missing context.
+ */
+ "quoteUnavailable": string;
+ };
}
declare const locales: {
[lang: string]: Locale;
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 66ca935b1b..c233e3ab87 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -5,6 +5,7 @@ introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato,
poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>."
monthAndDay: "{day}/{month}"
search: "Cerca"
+reset: "Ripristinare"
notifications: "Notifiche"
username: "Nome utente"
password: "Password"
@@ -48,6 +49,7 @@ pin: "Fissa sul profilo"
unpin: "Non fissare sul profilo"
copyContent: "Copia il contenuto"
copyLink: "Copia il link"
+copyRemoteLink: "Copia link remoto"
copyLinkRenote: "Copia collegamento alla Rinota"
delete: "Elimina"
deleteAndEdit: "Elimina e modifica"
@@ -56,7 +58,7 @@ addToList: "Aggiungi alla lista"
addToAntenna: "Aggiungi all'antenna"
sendMessage: "Invia messaggio"
copyRSS: "Copia RSS"
-copyUsername: "Copia nome utente"
+copyUsername: "Copia indirizzo del profilo"
copyUserId: "Copia ID del profilo"
copyNoteId: "Copia ID della Nota"
copyFileId: "Copia ID del file"
@@ -105,7 +107,7 @@ makeFollowManuallyApprove: "Approva i follower manualmente"
defaultNoteVisibility: "Privacy predefinita delle note"
follow: "Segui"
followRequest: "Richiesta di follow"
-followRequests: "Richieste di follow"
+followRequests: "Relazioni"
unfollow: "Togli Following"
followRequestPending: "Richiesta in approvazione"
enterEmoji: "Inserisci emoji"
@@ -440,7 +442,7 @@ recentlyRegisteredUsers: "Profili iscritti di recente"
recentlyDiscoveredUsers: "Profili scoperti di recente"
exploreUsersCount: "Ci sono {count} profili"
exploreFediverse: "Esplora il Fediverso"
-popularTags: "Tag di tendenza"
+popularTags: "Hashtag popolari"
userList: "Liste"
about: "Informazioni"
aboutMisskey: "Informazioni di Misskey"
@@ -535,7 +537,7 @@ regenerate: "Generare di nuovo"
fontSize: "Dimensione carattere"
mediaListWithOneImageAppearance: "Altezza dell'elenco media con una sola immagine "
limitTo: "Limita a {x}"
-noFollowRequests: "Non hai alcuna richiesta di follow"
+noFollowRequests: "Non ci sono richieste di relazione"
openImageInNewTab: "Apri le immagini in un nuovo tab"
dashboard: "Pannello di controllo"
local: "Locale"
@@ -551,8 +553,8 @@ promote: "Pubblicizza"
numberOfDays: "Numero di giorni"
hideThisNote: "Nasconda la nota"
showFeaturedNotesInTimeline: "Mostrare le note di tendenza nella tua timeline"
-objectStorage: "Stoccaggio oggetti"
-useObjectStorage: "Utilizza stoccaggio oggetti"
+objectStorage: "Storage S3"
+useObjectStorage: "Utilizza lo storage S3 in cloud"
objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "URL di riferimento. In caso di utilizzo di proxy o CDN l'URL è 'https://<bucket>.s3.amazonaws.com' per S3, 'https://storage.googleapis.com/<bucket>' per GCS eccetera. "
objectStorageBucket: "Bucket"
@@ -586,6 +588,7 @@ masterVolume: "Volume principale"
notUseSound: "Non emettere suoni"
useSoundOnlyWhenActive: "Emetti suoni solo quando Misskey è in attività"
details: "Dettagli"
+renoteDetails: "Dettagli della Rinota"
chooseEmoji: "Scegli emoji"
unableToProcess: "Impossibile compiere l'operazione"
recentUsed: "Usato di recente"
@@ -683,11 +686,15 @@ smtpSecure: "Usare SSL/TLS implicito per le connessioni SMTP"
smtpSecureInfo: "Disabilitare quando è attivo STARTTLS."
testEmail: "Verifica il funzionamento"
wordMute: "Filtri parole"
+wordMuteDescription: "Contrae le Note con la parola o la frase specificata. Permette di espandere le Note, cliccandole."
hardWordMute: "Filtro parole forte"
+showMutedWord: "Elenca le parole silenziate"
+hardWordMuteDescription: "Nasconde le Note con la parola o la frase specificata. A differenza delle parole silenziate, la Nota non verrà federata."
regexpError: "errore regex"
regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:"
instanceMute: "Silenziare l'istanza"
userSaysSomething: "{name} ha detto qualcosa"
+userSaysSomethingAbout: "{name} ha Notato a riguardo di \"{word}\""
makeActive: "Attiva"
display: "Visualizza"
copy: "Copia"
@@ -699,7 +706,7 @@ database: "Base dati"
channel: "Canale"
create: "Crea"
notificationSetting: "Impostazioni notifiche"
-notificationSettingDesc: "Seleziona il tipo di notifiche da visualizzare."
+notificationSettingDesc: "Scegli quali notifiche mostrare."
useGlobalSetting: "Usa impostazioni generali"
useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifiche del profilo. Altrimenti si possono segliere impostazioni personalizzate."
other: "Eccetera"
@@ -906,7 +913,7 @@ itsOn: "Abilitato"
itsOff: "Disabilitato"
on: "Acceso"
off: "Spento"
-emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi"
+emailRequiredForSignup: "L'indirizzo e-mail è obbligatorio per registrarsi"
unread: "Non lette"
filter: "Filtri"
controlPanel: "Pannello di controllo"
@@ -969,7 +976,7 @@ requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso
isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema"
typeToConfirm: "Digita {x} per continuare"
deleteAccount: "Eliminazione profilo"
-document: "Documento"
+document: "Documentazione"
numberOfPageCache: "Numero di pagine cache"
numberOfPageCacheDescription: "Aumenta l'usabilità, ma aumenta anche il carico e l'utilizzo della memoria."
logoutConfirm: "Vuoi davvero uscire da Misskey? "
@@ -1105,7 +1112,7 @@ accountMovedShort: "Questo profilo è stato migrato"
operationForbidden: "Operazione non consentita"
forceShowAds: "Mostra sempre i banner"
addMemo: "Aggiungi Memo"
-editMemo: "Modifica Memo"
+editMemo: "Modifica il promemoria"
reactionsList: "Chi ha reagito?"
renotesList: "Chi ha Rinotato?"
notificationDisplay: "Stile delle notifiche"
@@ -1139,7 +1146,7 @@ options: "Opzioni del ruolo"
specifyUser: "Profilo specifico"
lookupConfirm: "Vuoi davvero richiedere informazioni?"
openTagPageConfirm: "Vuoi davvero aprire la pagina dell'hashtag?"
-specifyHost: "Specifica l'host"
+specifyHost: "Host specifici"
failedToPreviewUrl: "Anteprima non disponibile"
update: "Aggiorna"
rolesThatCanBeUsedThisEmojiAsReaction: "Ruoli che possono usare questa emoji come reazione"
@@ -1239,7 +1246,7 @@ code: "Codice"
reloadRequiredToApplySettings: "Per applicare le impostazioni, occorre ricaricare."
remainingN: "Rimangono: {n}"
overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?"
-seasonalScreenEffect: "Schermate in base alla stagione"
+seasonalScreenEffect: "Abilita gli effetti speciali stagionali"
decorate: "Decora"
addMfmFunction: "Aggiungi decorazioni"
enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM"
@@ -1298,6 +1305,10 @@ yourNameContainsProhibitedWordsDescription: "Se desideri comunque utilizzare que
thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autore richiede di iscriversi per vedere il contenuto"
lockdown: "Isolamento"
pleaseSelectAccount: "Per favore, seleziona un profilo"
+availableRoles: "Ruoli disponibili"
+acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento."
+federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione."
+federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server."
_accountSettings:
requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione"
requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler."
@@ -1454,6 +1465,8 @@ _serverSettings:
reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis."
inquiryUrl: "URL di contatto"
inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
+ openRegistration: "Registrazioni aperte"
+ openRegistrationWarning: "L’apertura della registrazione comporta dei rischi. Ti consigliamo di attivarla solo se hai predisposto il monitoraggio continuo del tuo server e puoi rispondere immediatamente se si verifica un problema."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo."
_accountMigration:
moveFrom: "Migra un altro profilo dentro a questo"
@@ -1920,10 +1933,10 @@ _serverDisconnectedBehavior:
quiet: "Visualizza avviso in modo discreto"
_channel:
create: "Nuovo canale"
- edit: "Gerisci canale"
+ edit: "Modifica il canale"
setBanner: "Scegli intestazione"
removeBanner: "Rimuovi intestazione"
- featured: "Di tendenza"
+ featured: "Popolari nel canale"
owned: "I miei canali"
following: "Following"
usersCount: "{n} partecipanti"
@@ -1948,7 +1961,7 @@ _instanceMute:
_theme:
explore: "Esplora temi"
install: "Installa un tema"
- manage: "Gestione temi"
+ manage: "Gestione dei temi"
code: "Codice tema"
description: "Descrizione"
installed: "{name} è installato"
@@ -2095,12 +2108,12 @@ _permissions:
"read:messaging": "Visualizzare la chat"
"write:messaging": "Gestire la chat"
"read:mutes": "Vedi i profili silenziati"
- "write:mutes": "Gestisci i profili silenziati"
+ "write:mutes": "Gestione dei profili silenziati"
"write:notes": "Creare / Eliminare note"
"read:notifications": "Visualizzare notifiche"
- "write:notifications": "Gestire notifiche"
+ "write:notifications": "Gestione delle notifiche"
"read:reactions": "Vedi reazioni"
- "write:reactions": "Gerisci reazioni"
+ "write:reactions": "Gestione delle reazioni"
"write:votes": "Votare"
"read:pages": "Visualizzare pagine"
"write:pages": "Gestire pagine"
@@ -2109,7 +2122,7 @@ _permissions:
"read:user-groups": "Vedere i gruppi di utenti"
"write:user-groups": "Gestire i gruppi di utenti"
"read:channels": "Visualizza canali"
- "write:channels": "Gerisci canali"
+ "write:channels": "Gestione dei canali"
"read:gallery": "Visualizza la galleria."
"write:gallery": "Gestione della galleria"
"read:gallery-likes": "Visualizza i contenuti della galleria."
@@ -2200,7 +2213,7 @@ _widgets:
notifications: "Notifiche"
timeline: "Timeline"
calendar: "Calendario"
- trends: "Di tendenza"
+ trends: "Hashtag popolari"
clock: "Orologio"
rss: "Lettura RSS"
rssTicker: "Nastro RSS"
@@ -2352,9 +2365,6 @@ _pages:
newPage: "Crea pagina"
editPage: "Modifica pagina"
readPage: "Visualizzando fonte "
- created: "Pagina creata!"
- updated: "Pagina aggiornata con successo!"
- deleted: "Pagina eliminata"
pageSetting: "Impostazioni pagina"
nameAlreadyExists: "Esiste già una pagina con lo stesso URL."
invalidNameTitle: "L'URL di pagina definito non è valido"
@@ -2440,13 +2450,13 @@ _notification:
quote: "Cita"
reaction: "Reazioni"
pollEnded: "Sondaggio chiuso."
- receiveFollowRequest: "Richiesta di follow ricevuta"
- followRequestAccepted: "Richiesta di follow accettata"
+ receiveFollowRequest: "Richieste di follow in arrivo"
+ followRequestAccepted: "Richieste di follow accettate"
roleAssigned: "Ruolo concesso"
achievementEarned: "Risultato raggiunto"
exportCompleted: "Esportazione completata"
- login: "Accedi"
- test: "Prova la notifica"
+ login: "Accessi"
+ test: "Notifiche di test"
app: "Notifiche da applicazioni"
_actions:
followBack: "Following ricambiato"
@@ -2716,6 +2726,66 @@ _contextMenu:
app: "Applicazione"
appWithShift: "Applicazione Shift+Tasto"
native: "Interfaccia utente del browser"
+_gridComponent:
+ _error:
+ requiredValue: "Campo obbligatorio"
+ columnTypeNotSupport: "Solo le colonne type:text permettono la convalida delle Espresioni Regolari"
+ patternNotMatch: "Il valore non coincide con {pattern}"
+ notUnique: "Il valore deve essere univoco"
+_roleSelectDialog:
+ notSelected: "Niente selezioato"
+_customEmojisManager:
+ _gridCommon:
+ copySelectionRows: "Copia le righe selezionate"
+ copySelectionRanges: "Copia l'intervallo selezionato"
+ deleteSelectionRows: "Elimina le righe selezionate"
+ deleteSelectionRanges: "Elimina le righe nell'intervallo selezionato"
+ searchSettings: "Impostazioni di ricerca"
+ searchSettingCaption: "Imposta condizioni di ricerca dettagliate."
+ searchLimit: "Risultati visualizzati"
+ sortOrder: "Ordine"
+ registrationLogs: "Storico della registrazione"
+ registrationLogsCaption: "Lo storico verrà visualizzato in base alla attività sulle emoji. Scompare quando si esegue un'operazione di aggiornamento/eliminazione o si modifica/ricarica la pagina."
+ alertEmojisRegisterFailedDescription: "Attenzione, è impossibile modificare la emoji. Si prega di controllare lo storico per ulteriori dettagli."
+ _logs:
+ showSuccessLogSwitch: "Mostra le azioni a buon fine"
+ failureLogNothing: "Non ci sono errori nello storico delle emoji"
+ logNothing: "Lo storico è vuoto."
+ _remote:
+ selectionRowDetail: "Dettagli della riga selezionata"
+ importSelectionRows: "Importa le righe selezionate"
+ importSelectionRangesRows: "Importa le righe nell'intervallo selezionato"
+ importEmojisButton: "Importa le emoji selezionate"
+ confirmImportEmojisTitle: "Importazione emoji"
+ confirmImportEmojisDescription: "Importazione di {count} emoji ricevute da remoto. Si prega di prestare molta attenzione al tipo di licenza delle emoji. Vuoi confermare?"
+ _local:
+ tabTitleList: "Elenco delle emoji registrate"
+ tabTitleRegister: "Registrazione emoji"
+ _list:
+ emojisNothing: "Non ci sono emoji registrate."
+ markAsDeleteTargetRows: "Selezionare le righe come eliminabili"
+ markAsDeleteTargetRanges: "Selezionare le righe nell'intervallo come eliminabili"
+ alertUpdateEmojisNothingDescription: "Non ci sono emoji aggiornate."
+ alertDeleteEmojisNothingDescription: "Non ci sono emoji da eliminare."
+ confirmMovePage: "Vuoi davvero spostare la pagina?"
+ confirmChangeView: "Vuoi davvero cambiare la vista?"
+ confirmUpdateEmojisDescription: "Aggiornamento di {count} emoji. Vuoi davvero continuare?"
+ confirmDeleteEmojisDescription: "Eliminazione delle {count} emoji selezionate. Vuoi davvero continuare?"
+ confirmResetDescription: "Verranno ripristinate tutte le modifiche apportate finora."
+ confirmMovePageDesciption: "Sono state modificate le emoji in questa pagina.\nUscendo senza salvare, tutte le modifiche verranno ignorate."
+ dialogSelectRoleTitle: "Cerca emoji per ruolo"
+ _register:
+ uploadSettingTitle: "Caricamento impostazioni"
+ uploadSettingDescription: "Questa schermata ti permette di scegliere il comportamento durante il caricamento delle emoji."
+ directoryToCategoryLabel: "Inseriscile in una cartella omonima alla categoria"
+ directoryToCategoryCaption: "Crea il campo categoria in base alla cartella."
+ emojiInputAreaCaption: "Seleziona l'emoji da registrare utilizzando uno dei metodi."
+ emojiInputAreaList1: "Trascina una immagine o una cartella in quest'area"
+ emojiInputAreaList2: "Clicca per scegliere file dal tuo dispositivo"
+ emojiInputAreaList3: "Clicca per selezionare dal Drive"
+ confirmRegisterEmojisDescription: "Registrazione delle emoji elencate come nuove emoji personalizzate. Vuoi davvero procedere? (Per evitare sovraccarichi, puoi registrare al massimo {count} emoji per volta)"
+ confirmClearEmojisDescription: "Annullare le modifiche e cancella le emoji nell'elenco. Confermi?"
+ confirmUploadEmojisDescription: "Caricamento sul Drive di {count} file locali. Vuoi davvero procedere?"
_embedCodeGen:
title: "Personalizza il codice di incorporamento"
header: "Mostra la testata"
@@ -2736,3 +2806,37 @@ _selfXssPrevention:
description1: "Incollando qualcosa qui, malintenzionati potrebbero prendere il controllo del tuo profilo o rubare i tuoi dati personali."
description2: "Se non sai esattamente cosa stai facendo, %c smetti subito e chiudi questa finestra."
description3: "Per favore, controlla questo collegamento per avere maggiori dettagli. {link}"
+_followRequest:
+ recieved: "Ricezione richiesta di Follow"
+ sent: "Richiesta di Follow, inviata"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "Server irraggiungibile"
+ description: "La comunicazione con questo server potrebbe essere disattivata. Hai bloccato il server? Oppure potrebbero averlo bloccato gli amministratori. Contattali per ulteriori informazioni."
+ _uriInvalid:
+ title: "URL non valido"
+ description: "Controlla che l'indirizzo sia valido e sia privo di caratteri non validi."
+ _requestFailed:
+ title: "Richiesta fallita"
+ description: "La comunicazione col server non è riuscita. Potrebbe essere inattivo. Assicurati anche che la URL sia valida."
+ _responseInvalid:
+ title: "Risposta non valida"
+ description: "La comunicazione col server è andata a buon fine, ma abbiamo ricevuto dati non validi."
+ _responseInvalidIdHostNotMatch:
+ description: "L'indirizzo immesso non coincide con la URL finale. Interrogando i server per un contenuto remoto, assicurarsi di utilizzare la URL finale e non quella di un server intermedio."
+ _noSuchObject:
+ title: "Non trovato"
+ description: "La risorsa richiesta non è stata trovata. Verificare nuovamente la URL."
+_captcha:
+ verify: "Per favore, controlla la verifica CAPTCHA"
+ testSiteKeyMessage: "Puoi provare l'anteprima inserendo valori di test, sia per la chiave del sito che per la chiave segreta.\nSi prega di controllare la pagina qui sotto per i dettagli."
+ _error:
+ _requestFailed:
+ title: "Errore durante la richiesta del CAPTCHA"
+ text: "Riprova più tardi o controlla nuovamente le impostazioni."
+ _verificationFailed:
+ title: "Convalida CAPTCHA non riuscita"
+ text: "Si prega di verificare nuovamente se le impostazioni sono corrette."
+ _unknown:
+ title: "Errore CAPTCHA"
+ text: "Si è verificato un errore imprevisto."
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1b59708d85..13d8aec9b8 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -5,6 +5,7 @@ introMisskey: "ようこそ!Misskeyは、オープンソースの分散型マ
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつです。"
monthAndDay: "{month}月 {day}日"
search: "検索"
+reset: "リセット"
notifications: "通知"
username: "ユーザー名"
password: "パスワード"
@@ -48,6 +49,7 @@ pin: "ピン留め"
unpin: "ピン留め解除"
copyContent: "内容をコピー"
copyLink: "リンクをコピー"
+copyRemoteLink: "リモートのリンクをコピー"
copyLinkRenote: "リノートのリンクをコピー"
delete: "削除"
deleteAndEdit: "削除して編集"
@@ -684,11 +686,15 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト"
wordMute: "ワードミュート"
+wordMuteDescription: "指定した語句を含むノートを最小化します。最小化されたノートをクリックすることで表示することができます。"
hardWordMute: "ハードワードミュート"
+showMutedWord: "ミュートされたワードを表示"
+hardWordMuteDescription: "指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。"
regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
instanceMute: "サーバーミュート"
userSaysSomething: "{name}が何かを言いました"
+userSaysSomethingAbout: "{name}が「{word}」について何かを言いました"
makeActive: "アクティブにする"
display: "表示"
copy: "コピー"
@@ -1038,7 +1044,7 @@ youCannotCreateAnymore: "これ以上作成することはできません。"
cannotPerformTemporary: "一時的に利用できません"
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
invalidParamError: "パラメータエラー"
-invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。"
+invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる・許可されていない文字を入力している等の可能性もあります。"
permissionDeniedError: "操作が拒否されました"
permissionDeniedErrorDescription: "このアカウントにはこの操作を行うための権限がありません。"
preset: "プリセット"
@@ -1301,6 +1307,8 @@ lockdown: "ロックダウン"
pleaseSelectAccount: "アカウントを選択してください"
availableRoles: "利用可能なロール"
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
+federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
+federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
@@ -2414,9 +2422,6 @@ _pages:
newPage: "ページの作成"
editPage: "ページの編集"
readPage: "ソースを表示中"
- created: "ページを作成しました"
- updated: "ページを更新しました"
- deleted: "ページを削除しました"
pageSetting: "ページ設定"
nameAlreadyExists: "指定されたページURLは既に存在しています"
invalidNameTitle: "不正なページURLです"
@@ -2801,6 +2806,69 @@ _contextMenu:
appWithShift: "Shiftキーでアプリケーション"
native: "ブラウザのUI"
+_gridComponent:
+ _error:
+ requiredValue: "この値は必須項目です"
+ columnTypeNotSupport: "正規表現によるバリデーションはtype:textのカラムのみサポートします。"
+ patternNotMatch: "この値は{pattern}のパターンに一致しません"
+ notUnique: "この値は一意である必要があります"
+
+_roleSelectDialog:
+ notSelected: "選択されていません"
+
+_customEmojisManager:
+ _gridCommon:
+ copySelectionRows: "選択行をコピー"
+ copySelectionRanges: "選択範囲をコピー"
+ deleteSelectionRows: "選択行を削除"
+ deleteSelectionRanges: "選択範囲の値をクリア"
+ searchSettings: "検索設定"
+ searchSettingCaption: "検索条件を詳細に設定します。"
+ searchLimit: "表示件数"
+ sortOrder: "並び順"
+ registrationLogs: "登録ログ"
+ registrationLogsCaption: "絵文字更新・削除時のログが表示されます。更新・削除操作を行ったり、ページを遷移・リロードすると消えます。"
+ alertEmojisRegisterFailedDescription: "絵文字の更新・削除に失敗しました。詳細は登録ログをご確認ください。"
+ _logs:
+ showSuccessLogSwitch: "成功ログを表示"
+ failureLogNothing: "失敗ログはありません。"
+ logNothing: "ログはありません。"
+ _remote:
+ selectionRowDetail: "選択行の詳細"
+ importSelectionRows: "選択行をインポート"
+ importSelectionRangesRows: "選択範囲の行をインポート"
+ importEmojisButton: "チェックされた絵文字をインポート"
+ confirmImportEmojisTitle: "絵文字のインポート"
+ confirmImportEmojisDescription: "リモートから受信した{count}個の絵文字のインポートを行います。絵文字のライセンスに十分な注意を払ってください。実行しますか?"
+ _local:
+ tabTitleList: "登録済み絵文字一覧"
+ tabTitleRegister: "絵文字の登録"
+ _list:
+ emojisNothing: "登録された絵文字はありません。"
+ markAsDeleteTargetRows: "選択行を削除対象にする"
+ markAsDeleteTargetRanges: "選択範囲の行を削除対象にする"
+ alertUpdateEmojisNothingDescription: "変更された絵文字はありません。"
+ alertDeleteEmojisNothingDescription: "削除対象の絵文字はありません。"
+ confirmMovePage: "ページを移動しますか?"
+ confirmChangeView: "表示を変更しますか?"
+ confirmUpdateEmojisDescription: "{count}個の絵文字を更新します。実行しますか?"
+ confirmDeleteEmojisDescription: "チェックがつけられた{count}個の絵文字を削除します。実行しますか?"
+ confirmResetDescription: "今までに加えた変更がすべてリセットされます。"
+ confirmMovePageDesciption: "このページの絵文字に変更が加えられています。\n保存せずにこのままページを移動すると、このページで加えた変更はすべて破棄されます。"
+ dialogSelectRoleTitle: "絵文字に設定されたロールで検索"
+ _register:
+ uploadSettingTitle: "アップロード設定"
+ uploadSettingDescription: "この画面で絵文字アップロードを行う際の動作を設定できます。"
+ directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する"
+ directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。"
+ emojiInputAreaCaption: "いずれかの方法で登録する絵文字を選択してください。"
+ emojiInputAreaList1: "この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ"
+ emojiInputAreaList2: "このリンクをクリックしてPCから選択する"
+ emojiInputAreaList3: "このリンクをクリックしてドライブから選択する"
+ confirmRegisterEmojisDescription: "リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)"
+ confirmClearEmojisDescription: "編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?"
+ confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?"
+
_embedCodeGen:
title: "埋め込みコードをカスタマイズ"
header: "ヘッダーを表示"
@@ -2826,3 +2894,36 @@ _selfXssPrevention:
_followRequest:
recieved: "受け取った申請"
sent: "送った申請"
+
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "このサーバーとは通信できません"
+ description: "このサーバーとの通信が無効化されているか、このサーバーをブロックしている・ブロックされている可能性があります。\nサーバー管理者にお問い合わせください。"
+ _uriInvalid:
+ title: "URIが不正です"
+ description: "入力されたURIに問題があります。URIに使用できない文字を入力していないか確認してください。"
+ _requestFailed:
+ title: "リクエストに失敗しました"
+ description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。"
+ _responseInvalid:
+ title: "レスポンスが不正です"
+ description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。"
+ _responseInvalidIdHostNotMatch:
+ description: "入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。"
+ _noSuchObject:
+ title: "見つかりません"
+ description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。"
+
+_captcha:
+ verify: "CAPTCHAを通過してください"
+ testSiteKeyMessage: "サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できます。\n詳細は下記ページをご確認ください。"
+ _error:
+ _requestFailed:
+ title: "CAPTCHAのリクエストに失敗しました"
+ text: "しばらく後に実行するか、設定をもう一度ご確認ください。"
+ _verificationFailed:
+ title: "CAPTCHAの検証に失敗しました"
+ text: "設定が正しいかどうかもう一度確認ください。"
+ _unknown:
+ title: "CAPTCHAエラー"
+ text: "想定外のエラーが発生しました。"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index c3e0096926..66560f524b 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -15,7 +15,7 @@ forgotPassword: "パスワード忘れたん?"
fetchingAsApObject: "今ちと連合に照会しとるで"
ok: "ええで"
gotIt: "ほい"
-cancel: "やめとく"
+cancel: "やめる"
noThankYou: "やめとく"
enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がリノートしたで"
@@ -26,7 +26,7 @@ settings: "設定"
notificationSettings: "通知の設定"
basicSettings: "基本設定"
otherSettings: "ほかの設定"
-openInWindow: "ウィンドウで開くで"
+openInWindow: "ウィンドウで開く"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介食ってもた"
@@ -45,7 +45,7 @@ favorited: "お気に入りに入れたで。"
alreadyFavorited: "もうお気に入りに入れとるがな。"
cantFavorite: "アカン、お気に入りに入れれんかったわ。"
pin: "ピン留めしとく"
-unpin: "やっぱピン留めせん"
+unpin: "ピン留めやめる"
copyContent: "内容をコピー"
copyLink: "リンクをコピー"
copyLinkRenote: "リノートのリンクをコピーするで?"
@@ -63,7 +63,7 @@ copyFileId: "ファイルIDをコピー"
copyFolderId: "フォルダーIDをコピー"
copyProfileUrl: "プロフィールURLをコピー"
searchUser: "ユーザーを探す"
-searchThisUsersNotes: "ユーザーのノートを検索"
+searchThisUsersNotes: "ユーザーのノートを探す"
reply: "返事"
loadMore: "まだまだあるで!"
showMore: "まだまだあるで!"
@@ -138,8 +138,8 @@ reactionSettingDescription2: "ドラッグで並び替え、クリックで削
rememberNoteVisibility: "公開範囲覚えといて"
attachCancel: "のっけるのやめる"
deleteFile: "ファイルをほかす"
-markAsSensitive: "ちょっとこれはアカン"
-unmarkAsSensitive: "そこまでアカンことないやろ"
+markAsSensitive: "ちょっと見せられへんわ"
+unmarkAsSensitive: "別にええんじゃね?"
enterFileName: "ファイル名を入れてや"
mute: "ミュート"
unmute: "ミュートやめたる"
@@ -152,13 +152,13 @@ unsuspend: "溶かす"
blockConfirm: "ブロックしてもええんか?"
unblockConfirm: "ブロックやめたるってほんまか?"
suspendConfirm: "凍結してしもうてええか?"
-unsuspendConfirm: "解凍するけどええか?"
+unsuspendConfirm: "溶かしたるけどええか?"
selectList: "リストを選ぶ"
editList: "リストいじる"
selectChannel: "チャンネルを選ぶ"
selectAntenna: "アンテナを選ぶ"
editAntenna: "アンテナいじる"
-createAntenna: "アンテナを作成"
+createAntenna: "アンテナを作る"
selectWidget: "ウィジェットを選ぶ"
editWidgets: "ウィジェットをいじる"
editWidgetsExit: "いじるのをやめる"
@@ -172,12 +172,12 @@ settingGuide: "ええ感じの設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を入れとったら、リモートのファイルを端から端までこのサーバーのキャッシュん中突っ込むようになるで。画像映し出すんがめっちゃ速うなるけど、サーバーの容量をやたらと食うようになるで。リモートの人がどんだけ長くキャッシュを持っとくかはドライブ容量の制限で決めとくで。制限を超えたら古いのから順々に消してって、かわりにリンクになるで。この設定を切ったら、リモートのファイルは最初っからリンクとして扱うことにするけど、画像のサムネ作るのとかみんなのプライバシー守るために、default.ymlのproxyRemoteFilesをtrueにしといたほうがええよ。"
youCanCleanRemoteFilesCache: "ファイル管理にある🗑️ボタンでキャッシュ全部ほかすで。"
-cacheRemoteSensitiveFiles: "リモートのきわどいファイルをキャッシュに突っ込む"
+cacheRemoteSensitiveFiles: "リモートのきわどいファイルをキャッシュする"
cacheRemoteSensitiveFilesDescription: "この設定を切ると、リモートのきわどいファイルはキャッシュせず直でリンクするようになるで。"
flagAsBot: "Botにするで"
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
flagAsCat: "猫や。かわええな。"
-flagAsCatDescription: "ネコになりたいんならこれつけとき。"
+flagAsCatDescription: "猫になりたいんならこれつけとき。"
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
flagShowTimelineRepliesDescription: "オンにしたら、タイムラインにユーザーのノートの他にもそのユーザーの他のノートへの返信を表示するで。"
autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく"
@@ -186,9 +186,9 @@ reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗してもうた…"
showOnRemote: "リモートで見る"
continueOnRemote: "リモートで続行"
-chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
+chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選ぶ"
specifyServerHost: "サーバーのドメインを直接指定"
-inputHostName: "ドメインを入力せえや"
+inputHostName: "ドメインを入力してや"
general: "全般"
wallpaper: "壁紙"
setWallpaper: "壁紙を設定"
@@ -586,6 +586,7 @@ masterVolume: "全体のやかましさ"
notUseSound: "音出さへん"
useSoundOnlyWhenActive: "Misskeyがアクティブなときだけ音出す"
details: "もっと"
+renoteDetails: "リノートの詳細"
chooseEmoji: "絵文字を選ぶ"
unableToProcess: "なんか奥の方で詰まってもうた"
recentUsed: "最近使ったやつ"
@@ -946,6 +947,9 @@ oneHour: "1時間"
oneDay: "1日"
oneWeek: "1週間"
oneMonth: "1ヶ月"
+threeMonths: "3ヶ月"
+oneYear: "1年"
+threeDays: "3日"
reflectMayTakeTime: "反映されるまで時間がかかることがあるで"
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
rateLimitExceeded: "レート制限が超えたみたいやで"
@@ -1292,6 +1296,23 @@ prohibitedWordsForNameOfUser: "禁止ワード(ユーザー名)"
prohibitedWordsForNameOfUserDescription: "このリストの中にある文字列がユーザー名に入っとったら、その名前に変更できひんようになるで。モデレーター権限があるユーザーは除外や。"
yourNameContainsProhibitedWords: "その名前は禁止した文字列が含まれとるで"
yourNameContainsProhibitedWordsDescription: "その名前は禁止した文字列が含まれとるわ。どうしてもって言うなら、サーバー管理者に言うしかないで。"
+thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者が、表示にログインが要るって設定してるで"
+lockdown: "ロックダウン"
+pleaseSelectAccount: "アカウント選んでや"
+availableRoles: "使えるロール"
+acknowledgeNotesAndEnable: "注意事項をわかった上でオンにする。"
+_accountSettings:
+ requireSigninToViewContents: "ログインしてもらってからコンテンツ見てもらう"
+ requireSigninToViewContentsDescription1: "あなたが作成した全部のノートとかのコンテンツを見れるようにするのにログインがいるようにするで。クローラーにいろいろ収集されるんを防げるかもしれん。"
+ requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応してないサーバーからの表示ができんくなるで。"
+ requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツは、これらの制限が適用されんかもしれんで。"
+ makeNotesFollowersOnlyBefore: "昔のノートをフォロワーだけに見てもらう"
+ makeNotesFollowersOnlyBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。"
+ makeNotesHiddenBefore: "昔のノートを見れんようにする"
+ makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。"
+ mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。"
+ notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート"
+ notesOlderThanSpecifiedDateAndTime: "決めた日時より前のノート"
_abuseUserReport:
forward: "転送"
forwardDescription: "匿名のシステムアカウントってことにして、リモートサーバーに通報を転送するで。"
@@ -1436,6 +1457,8 @@ _serverSettings:
reactionsBufferingDescription: "有効にしたら、リアクション作るときのパフォーマンスがすっごい上がって、データベースへの負荷が減るで。代わりに、Redisのメモリ使用は増えるで。"
inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。"
+ openRegistration: "アカウントの作成をオープンにする"
+ openRegistrationWarning: "登録を解放するのはリスクが伴うで。サーバーをいっつも監視して、なんか起きたらすぐに対応できるんやったら、オンにしてもええと思う。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターがおらんかったら、スパムを防ぐためにこの設定は勝手に切られるで。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに引っ越す"
@@ -2156,8 +2179,11 @@ _auth:
permissionAsk: "このアプリは次の権限を要求しとるで"
pleaseGoBack: "アプリケーションに戻ってええよ"
callback: "アプリケーションに戻っとるで"
+ accepted: "アクセスを許可したで"
denied: "アクセスを拒否ったで"
+ scopeUser: "以下のユーザーとしていじってるで"
pleaseLogin: "アプリにアクセスさせるんやったら、ログインしてや。"
+ byClickingYouWillBeRedirectedToThisUrl: "アクセスを許したら、自動で下のURLに遷移するで"
_antennaSources:
all: "みんなのノート"
homeTimeline: "フォローしとるユーザーのノート"
@@ -2331,9 +2357,6 @@ _pages:
newPage: "ページを作る"
editPage: "ページの編集"
readPage: "ソースを表示中"
- created: "ページを作成したで"
- updated: "ページを更新したで"
- deleted: "ページを削除したで"
pageSetting: "ページ設定"
nameAlreadyExists: "指定されたページURLはもうあるみたいや"
invalidNameTitle: "正しくないページURLみたいやで"
@@ -2709,3 +2732,30 @@ _embedCodeGen:
generateCode: "埋め込みコード作る"
codeGenerated: "コード作ったで"
codeGeneratedDescription: "作ったコードはウェブサイトに貼っつけて使ってや。"
+_selfXssPrevention:
+ warning: "警告"
+ title: "「この画面になんか貼り付けろ」は全部詐欺やで。"
+ description1: "ここになんかはつっつけると、悪いユーザーにアカウント乗っ取られたり、個人情報盗まれたりするかもやで"
+ description2: "はっつけようとしてるものがなんなんかわからんのやったら、%c今すぐ作業やめてウィンドウを閉じて。"
+ description3: "詳しくはこれを見て。{link}"
+_followRequest:
+ recieved: "もらった申請"
+ sent: "送った申請"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "このサーバーと通信できん"
+ description: "このサーバーとの通信は無効化されてるか、このサーバーをブロックしてるんか、ブロックされてるかもしれん。\nサーバー管理者に問い合わせてや。"
+ _uriInvalid:
+ title: "URIがおかしいで"
+ description: "入力されたURIに問題があるで。URIに使えん文字を入れてないから確かめて。"
+ _requestFailed:
+ title: "リクエスト失敗してもうたで"
+ description: "このサーバーとの通信に失敗してもうたわ。相手サーバーがダウンしてるかもしれん。あと、おかしいURIとか、ありえんURIを入れてないか確かめて。"
+ _responseInvalid:
+ title: "レスポンスがおかしいで"
+ description: "このサーバーと通信することはできたけど、もらったデータがおかしかったで。"
+ _responseInvalidIdHostNotMatch:
+ description: "入力されたURIのドメインと最終的に得られたURIのドメインとが違うで。第三者のサーバーを介してリモートのコンテンツを照会してるんやったら、発信元のサーバーで取得できるURIを使って照会し直して。"
+ _noSuchObject:
+ title: "見つからへんね"
+ description: "求められたリソースが見つからんかったで。URIをもっかい確かめてや。"
diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml
index 60b82d5db9..4b9650b636 100644
--- a/locales/ko-GS.yml
+++ b/locales/ko-GS.yml
@@ -840,3 +840,6 @@ _reversi:
black: "꺼멍"
white: "허영"
total: "합게"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "몬 찾앗십니다"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index d694d2dbae..36b818c117 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -5,6 +5,7 @@ introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로
poweredByMisskeyDescription: "{name} 서버는 오픈소스 플랫폼 <b>Misskey</b>의 서버 가운데 하나입니다."
monthAndDay: "{month}월 {day}일"
search: "검색"
+reset: "초기화"
notifications: "알림"
username: "유저명"
password: "비밀번호"
@@ -48,6 +49,7 @@ pin: "프로필에 고정"
unpin: "프로필에서 고정 해제"
copyContent: "내용 복사"
copyLink: "링크 복사"
+copyRemoteLink: "리모트 서버의 링크로 복사하기"
copyLinkRenote: "리노트 링크 복사"
delete: "삭제"
deleteAndEdit: "삭제 후 편집"
@@ -684,11 +686,15 @@ smtpSecure: "SMTP 연결에 Implicit SSL/TTS 사용"
smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다."
testEmail: "이메일 전송 테스트"
wordMute: "단어 뮤트"
+wordMuteDescription: "정해진 단어가 포함된 노트를 최소화 한 상태로 표시합니다. 최소화 된 노트는 클릭해서 표시할 수 있습니다."
hardWordMute: "하드 단어 뮤트"
+showMutedWord: "뮤트한 단어를 표시하기"
+hardWordMuteDescription: "정한 단어가 들어간 노트를 숨깁니다. 단어 뮤트와 차이점은 노트가 아예 보이지 않습니다."
regexpError: "정규 표현식 오류"
regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오류가 발생했습니다:"
instanceMute: "서버 뮤트"
userSaysSomething: "{name}님이 무언가를 말했습니다"
+userSaysSomethingAbout: "{name}님이 \"{word}\"를 언급했습니다."
makeActive: "활성화"
display: "보기"
copy: "복사"
@@ -1277,7 +1283,7 @@ confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확
sensitiveMediaRevealConfirm: "민감한 미디어입니다. 표시할까요?"
createdLists: "만든 리스트"
createdAntennas: "만든 안테나"
-fromX: "{x}부터"
+fromX: "{x}에서"
genEmbedCode: "임베디드 코드 만들기"
noteOfThisUser: "이 유저의 노트 목록"
clipNoteLimitExceeded: "더 이상 이 클립에 노트를 추가 할 수 없습니다."
@@ -1301,13 +1307,15 @@ lockdown: "잠금"
pleaseSelectAccount: "계정을 선택해주세요."
availableRoles: "사용 가능한 역할"
acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했습니다."
+federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다."
+federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다."
_accountSettings:
- requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
+ requireSigninToViewContents: "콘텐츠 열람을 위해 로그인을 필수로 설정하기"
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페이지에 삽입, 노트 인용을 지원하지 않는 서버에서 볼 수 없게 됩니다."
requireSigninToViewContentsDescription3: "원격 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다."
makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 볼 수 있도록 설정하기"
- makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다.비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
+ makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
makeNotesHiddenBefore: "과거 노트 비공개로 전환하기"
makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
mayNotEffectForFederatedNotes: "원격 서버에 연합된 노트에는 효과가 없을 수도 있습니다."
@@ -2357,9 +2365,6 @@ _pages:
newPage: "페이지 만들기"
editPage: "페이지 수정"
readPage: "소스 표시 중"
- created: "페이지를 만들었습니다"
- updated: "페이지를 수정했습니다"
- deleted: "페이지가 삭제되었습니다"
pageSetting: "페이지 설정"
nameAlreadyExists: "지정한 페이지 URL이 이미 존재합니다"
invalidNameTitle: "유효하지 않은 페이지 URL입니다"
@@ -2514,7 +2519,7 @@ _webhookSettings:
reaction: "누군가 내 노트에 리액션했을 때"
mention: "누군가 나를 멘션했을 때"
_systemEvents:
- abuseReport: "유저롭"
+ abuseReport: "유저로부터 신고를 받았을 때"
abuseReportResolved: "받은 신고를 처리했을 때"
userCreated: "유저가 생성되었을 때"
inactiveModeratorsWarning: "모더레이터가 일정 기간동안 활동하지 않은 경우"
@@ -2721,6 +2726,66 @@ _contextMenu:
app: "애플리케이션"
appWithShift: "Shift 키로 애플리케이션"
native: "브라우저의 UI"
+_gridComponent:
+ _error:
+ requiredValue: "이 값은 필수 항목입니다."
+ columnTypeNotSupport: "정규표현 규칙이 type:text인 칼럼만 지원합니다."
+ patternNotMatch: "이 값은 {pattern} 패턴과 일치하지 않습니다."
+ notUnique: "이 값은 다른 값과 중복되지 않아야 합니다."
+_roleSelectDialog:
+ notSelected: "선택하지 않았습니다."
+_customEmojisManager:
+ _gridCommon:
+ copySelectionRows: "선택한 행을 복사하기"
+ copySelectionRanges: "선택범위를 복사하기"
+ deleteSelectionRows: "선택한 행을 삭제"
+ deleteSelectionRanges: "선택한 행을 삭제"
+ searchSettings: "검색 설정"
+ searchSettingCaption: "고급 검색을 설정합니다."
+ searchLimit: "표시 건수"
+ sortOrder: "정렬 순서"
+ registrationLogs: "등록 로그"
+ registrationLogsCaption: "이모지를 갱신하거나 삭제할 때 로그가 표시됩니다. 갱신 또는 삭제하거나, 페이지 이동, 새로 고침하면 삭제됩니다."
+ alertEmojisRegisterFailedDescription: "이모지를 갱신 또는 삭제하지 못했습니다. 자세한 내용은 등록 로그를 확인해주세요."
+ _logs:
+ showSuccessLogSwitch: "성공 로그를 표시"
+ failureLogNothing: "실패 로그가 없습니다."
+ logNothing: "로그가 없습니다."
+ _remote:
+ selectionRowDetail: "선택 행 (상세)"
+ importSelectionRows: "선택 행을 가져오기"
+ importSelectionRangesRows: "선택한 범위 안의 행을 가져오기"
+ importEmojisButton: "선택한 이모지를 가져오기"
+ confirmImportEmojisTitle: "이모지 가져오기"
+ confirmImportEmojisDescription: "리모트 서버에서 받아온 이모지 {count}개를 이 서버로 가져옵니다. 이모지의 저작권, 라이선스를 확실히 확인하셨다면 실행해주세요."
+ _local:
+ tabTitleList: "등록한 이모지 리스트"
+ tabTitleRegister: "이모지 등록"
+ _list:
+ emojisNothing: "등록한 이모지가 없습니다."
+ markAsDeleteTargetRows: "선택한 행을 삭제할 대상으로 하기"
+ markAsDeleteTargetRanges: "선택한 범위의 행을 삭제 대상으로 하기"
+ alertUpdateEmojisNothingDescription: "변경할 이모지가 없습니다."
+ alertDeleteEmojisNothingDescription: "삭제 대상의 이모지는 없습니다."
+ confirmMovePage: "페이지를 이동할까요?"
+ confirmChangeView: "표시를 바꿀까요?"
+ confirmUpdateEmojisDescription: "{count}개의 이모지를 갱신합니다. 실행할까요?"
+ confirmDeleteEmojisDescription: "선택한 이모지 {count}개를 삭제합니다. 실행할까요?"
+ confirmResetDescription: "지금까지 했던 변경 내용이 모두 초기화됩니다."
+ confirmMovePageDesciption: "이 페이지의 이모지에 변경이 있습니다.\n저장하지 않은 상태로 페이지를 이동하면, 이 페이지에서 바꾼 변경 내용이 모두 지워집니다."
+ dialogSelectRoleTitle: "이모지에 설정된 역할을 검색"
+ _register:
+ uploadSettingTitle: "업로드 설정"
+ uploadSettingDescription: "여기서 이모지를 업로드 할 때의 동작을 설정할 수 있습니다."
+ directoryToCategoryLabel: "디렉토리 이름을 \"category\"로 입력하기"
+ directoryToCategoryCaption: "디렉토리를 드래그 앤 드롭한 경우, 디렉토리 이름을 \"category\"로 입력합니다."
+ emojiInputAreaCaption: "이모지를 등록할 방법을 선택해주세요."
+ emojiInputAreaList1: "이 틀 안에 이미지 파일 또는 디렉토리를 끌어서 가져오기"
+ emojiInputAreaList2: "이 링크를 클릭해서 PC에서 선택하기"
+ emojiInputAreaList3: "이 링크를 클릭해서 드라이브에서 선택하기"
+ confirmRegisterEmojisDescription: "리스트에 표시되어진 이모지를 새로운 커스텀 이모지로 등록합니다. 실행할까요? (부하를 피하기 위해, 한 번에 등록할 수 있는 이모지는 {count}건까지 입니다.)"
+ confirmClearEmojisDescription: "편집 내용을 지우고, 목록에 표시되어진 이모지를 지웁니다. 실행할까요?"
+ confirmUploadEmojisDescription: "드래그 앤 드롭한 {count}개의 파일을 드라이브에 업로드 합니다. 실행할까요?"
_embedCodeGen:
title: "임베디드 코드를 커스터마이즈"
header: "해더를 표시"
@@ -2744,3 +2809,34 @@ _selfXssPrevention:
_followRequest:
recieved: "받은 신청"
sent: "보낸 신청"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "이 서버와 통신할 수 없음"
+ description: "이 서버와의 통신이 비활성화 되었거나, 이 서버를 차단 중이거나 서버에게 차단되었을 수 있습니다.\n서버 관리자에게 문의하세요."
+ _uriInvalid:
+ title: "URI가 잘못되었습니다."
+ description: "입력한 URI에 문제가 있습니다. URI에 쓸 수 없는 문자를 넣었는지 확인해보세요."
+ _requestFailed:
+ title: "요청을 실패했습니다."
+ description: "해당 서버와 통신을 실패했습니다. 상대방 서버에 접속 불가능한 상태일 수도 있습니다. 또는 잘못된 URI 또는 없는 URI를 입력했는지 확인해보세요."
+ _responseInvalid:
+ title: "유효하지 않은 반응입니다."
+ description: "이 서버와 통신할 수 있지만, 데이터가 올바르지 않습니다."
+ _responseInvalidIdHostNotMatch:
+ description: "입력된 URI과 실제 URI가 다릅니다. 제 3자 서버를 통한 리모트 컨텐츠를 조회하는 경우, 원래 서버 측에서 받아올 수 있는 URI를 사용하여 조회하시길 바랍니다."
+ _noSuchObject:
+ title: "찾을 수 없습니다"
+ description: "요구된 리소스를 찾을 수 없습니다. URI를 다시 한 번 확인해보세요."
+_captcha:
+ verify: "CAPTCHA를 먼저 해결하세요."
+ testSiteKeyMessage: "사이트 키와 비밀 키에 테스트용 값을 입력하여 미리보기를 확인할 수 있습니다.\n자세한 내용은 아래 페이지를 확인해보세요."
+ _error:
+ _requestFailed:
+ title: "CAPTCHA 요구에 실패했습니다."
+ text: "잠시 후에 다시 실행하거나, 설정을 다시 한 번 확인해보세요."
+ _verificationFailed:
+ title: "CAPTCHA 검증을 실패했습니다."
+ text: "설정이 올바른지 다시 한 번 확인해보세요."
+ _unknown:
+ title: "CAPTCHA 에러"
+ text: "알 수 없는 에러가 발생했습니다."
diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml
index 38965119fe..2d55c289aa 100644
--- a/locales/lo-LA.yml
+++ b/locales/lo-LA.yml
@@ -474,3 +474,6 @@ _abuseReport:
mail: "ອີເມວ"
_moderationLogTypes:
suspend: "ລະງັບ"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "ບໍ່ພົບ"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
index 7e5e9cbbfb..685094b4a5 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -8,6 +8,9 @@ search: "Zoeken"
notifications: "Meldingen"
username: "Gebruikersnaam"
password: "Wachtwoord"
+initialPasswordForSetup: "Initiële wachtwoord voor configuratie"
+initialPasswordIsIncorrect: "Initiële wachtwoord voor configuratie is onjuist"
+initialPasswordForSetupDescription: "Gebruik het initiële wachtwoord uit de configuratie, als je Misskey zelf hebt geïnstalleerd.\nAls je een Misskey hosting provider gebruikt, gebruik dan het gegeven wachtwoord.\nAls je geen wachtwoord hebt gezet, laat het dan leeg om verder te gaan."
forgotPassword: "Wachtwoord vergeten"
fetchingAsApObject: "Ophalen vanuit de Fediverse"
ok: "Ok"
@@ -108,9 +111,12 @@ enterEmoji: "Voer een emoji in"
renote: "Herdelen"
unrenote: "Stop herdelen"
renoted: "Herdeeld"
+renotedToX: "Renoted naar {name}"
cantRenote: "Dit bericht kan niet worden herdeeld"
cantReRenote: "Een herdeling kan niet worden herdeeld"
quote: "Quote"
+renoteToChannel: "Renote naar kanaal"
+renoteToOtherChannel: "Renote naar ander kanaal"
pinnedNote: "Vastgemaakte notitie"
pinned: "Vastmaken aan profielpagina"
you: "Jij"
@@ -119,6 +125,10 @@ sensitive: "NSFW"
add: "Toevoegen"
reaction: "Reacties"
reactions: "Reacties"
+emojiPicker: "Emoji kiezer"
+pinnedEmojisForReactionSettingDescription: "Kies de emojis die als eerste getoond worden tijdens het reageren"
+pinnedEmojisSettingDescription: "Kies de emojis die als eerste getoond worden tijdens het reageren"
+emojiPickerDisplay: "Emoji kiezer weergave"
reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen"
rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen"
attachCancel: "Verwijder bijlage"
@@ -140,7 +150,7 @@ selectAntenna: "Kies een antenne"
selectWidget: "Kies een widget"
editWidgets: "Bewerk widgets"
editWidgetsExit: "Klaar"
-customEmojis: "Maatwerk emoji"
+customEmojis: "Eigen emoji"
emoji: "Emoji"
emojis: "Emoji"
emojiName: "Naam emoji"
@@ -403,7 +413,31 @@ help: "Help"
inputMessageHere: "Voer hier je bericht in"
close: "Sluiten"
invites: "Uitnodigen"
+members: "Leden"
+transfer: "Overdracht"
+title: "Titel"
+text: "Tekst"
+enable: "Inschakelen"
+next: "Volgende"
+retype: "Opnieuw invoeren"
+noteOf: "Notitie van {user}"
+quoteAttached: "Citaat"
+quoteQuestion: "Toevoegen als citaat?"
invitations: "Uitnodigen"
+dashboard: "Overzicht"
+local: "Lokaal"
+remote: "Remote"
+total: "Totaal"
+weekOverWeekChanges: "Wijzigingen sinds vorige week"
+dayOverDayChanges: "Dagelijkse wijzigingen"
+appearance: "Weergave"
+clientSettings: "Clientinstellingen"
+accountSettings: "Accountinstellingen"
+promotion: "Promotie"
+promote: "Promoot"
+numberOfDays: "Aantal dagen"
+hideThisNote: "Verberg deze notitie"
+showFeaturedNotesInTimeline: "Laat featured notities in tijdlijn zien"
sound: "Geluid"
smtpHost: "Server"
smtpUser: "Gebruikersnaam"
@@ -501,3 +535,8 @@ _webhookSettings:
_moderationLogTypes:
suspend: "Opschorten"
resetPassword: "Wachtwoord terugzetten"
+_reversi:
+ total: "Totaal"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Niet gevonden"
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
index 87ea01764d..474e05ba67 100644
--- a/locales/no-NO.yml
+++ b/locales/no-NO.yml
@@ -727,3 +727,6 @@ _abuseReport:
mail: "E-post"
_moderationLogTypes:
suspend: "Suspender"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Ikke funnet"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index 203f44b334..9bd585de86 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -1459,9 +1459,6 @@ _pages:
newPage: "Utwórz stronę"
editPage: "Edytuj tę stronę"
readPage: "Aktywowano widok źródła"
- created: "Pomyślnie utworzono stronę!"
- updated: "Pomyślnie zaktualizowano stronę!"
- deleted: "Strona została usunięta"
pageSetting: "Ustawienia strony"
nameAlreadyExists: "Określony adres URL strony już istnieje"
invalidNameTitle: "Podany adres URL strony jest nieprawidłowy"
@@ -1583,3 +1580,6 @@ _moderationLogTypes:
resetPassword: "Zresetuj hasło"
_reversi:
total: "Łącznie"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Nie znaleziono"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
index 7ef9e3a946..d691022d75 100644
--- a/locales/pt-PT.yml
+++ b/locales/pt-PT.yml
@@ -8,6 +8,9 @@ search: "Pesquisar"
notifications: "Notificações"
username: "Nome de usuário"
password: "Senha"
+initialPasswordForSetup: "Senha para a configuração inicial"
+initialPasswordIsIncorrect: "Senha para configuração inicial está incorreta"
+initialPasswordForSetupDescription: "Use a senha configurada no arquivo de configuração se você instalou o Misskey manualmente.\nSe você estiver utilizando um serviço de hospedagem, utilize a senha fornecida.\nSe uma senha não foi configurada, deixe em branco e continue."
forgotPassword: "Esqueci-me da senha"
fetchingAsApObject: "Buscando no Fediverso..."
ok: "OK"
@@ -196,7 +199,7 @@ followConfirm: "Tem certeza que quer seguir {name}?"
proxyAccount: "Conta proxy"
proxyAccountDescription: "Uma conta de proxy é uma conta que assume o acompanhamento remoto de um usuário sob certas condições específicas. Por exemplo, quando um usuário inclui um usuário remoto em uma lista, mas ninguém na lista está seguindo o usuário remoto, a atividade não é entregue ao servidor. Nesse caso, a conta de proxy entra em ação para seguir o usuário remoto em vez disso."
host: "Host"
-selectSelf: "Escolher manualmente"
+selectSelf: "Selecionar a mim"
selectUser: "Selecionar usuário"
recipient: "Destinatário"
annotation: "Anotação"
@@ -236,6 +239,8 @@ silencedInstances: "Instâncias silenciadas"
silencedInstancesDescription: "Liste o nome de hospedagem dos servidores que você deseja silenciar, separados por linha. Todas as contas desses servidores serão silenciada e poderão enviar solicitações para seguir, mas não poderão mencionar usuários locais sem segui-los. Isso não afetará servidores bloqueados."
mediaSilencedInstances: "Instâncias com mídia silenciadas"
mediaSilencedInstancesDescription: "Liste o nome de hospedagem dos servidores cuja mídia você deseja silenciar, separados por linha. Todas as contas desses servidores serão consideradas sensíveis e não poderão utilizar emojis personalizados. Isso não afetará servidores bloqueados."
+federationAllowedHosts: "Servidores com federação permitida"
+federationAllowedHostsDescription: "Especifique o endereço dos servidores em que deseja permitir a federação separados por linha."
muteAndBlock: "Silenciar e bloquear"
mutedUsers: "Usuários silenciados"
blockedUsers: "Usuários bloqueados"
@@ -334,6 +339,7 @@ renameFolder: "Renomear Pasta"
deleteFolder: "Excluir pasta"
folder: "Pasta"
addFile: "Adicionar arquivo"
+showFile: "Mostrar arquivos"
emptyDrive: "O drive está vazio"
emptyFolder: "A pasta está vazia"
unableToDelete: "Não é possível excluir"
@@ -447,6 +453,7 @@ totpDescription: "Digite a senha de uso único informado pelo aplicativo autenti
moderator: "Moderador"
moderation: "Moderação"
moderationNote: "Nota de moderação"
+moderationNoteDescription: "Você pode preencher notas que serão compartilhadas apenas com moderadores."
addModerationNote: "Adicionar nota de moderação"
moderationLogs: "Logs de moderação"
nUsersMentioned: "Postado por {n} pessoas"
@@ -508,6 +515,10 @@ uiLanguage: "Idioma de exibição da interface "
aboutX: "Sobre {x}"
emojiStyle: "Estilo de emojis"
native: "Nativo"
+menuStyle: "Estilo do menu"
+style: "Estilo"
+drawer: "Gaveta"
+popup: "Pop-up"
showNoteActionsOnlyHover: "Exibir as ações da nota somente ao passar o cursor sobre ela"
showReactionsCount: "Ver o número de reações nas notas"
noHistory: "Ainda não há histórico"
@@ -575,6 +586,7 @@ masterVolume: "volume principal"
notUseSound: "Desabilitar som"
useSoundOnlyWhenActive: "Apenas reproduzir sons quando Misskey estiver aberto."
details: "Detalhes"
+renoteDetails: "Detalhes da repostagem"
chooseEmoji: "Selecione um emoji"
unableToProcess: "Não é possível concluir a operação"
recentUsed: "Usado recentemente"
@@ -590,6 +602,8 @@ ascendingOrder: "Ascendente"
descendingOrder: "Descendente"
scratchpad: "Bloco de rascunho"
scratchpadDescription: "O Bloco de rascunho fornece um ambiente experimental para AiScript. Permite escrever, executar e verificar os resultados do código para interagir com o Misskey."
+uiInspector: "Inspecionador de interface"
+uiInspectorDescription: "Você pode ver a lista de servidores de componentes de interface na memória. Componentes da interface serão gerados pela função Ui:C:."
output: "Resultado"
script: "Script"
disablePagesScript: "Desabilitar scripts nas páginas"
@@ -670,7 +684,7 @@ smtpSecure: "Use SSL/TLS implícito para conexões SMTP"
smtpSecureInfo: "Desative esta opção ao utilizar STARTTLS."
testEmail: "Testar envio de e-mail"
wordMute: "Silenciar palavras"
-hardWordMute: "SIlenciamento pesado de palavra"
+hardWordMute: "Silenciar palavras (esconder posts)"
regexpError: "Erro na expressão regular"
regexpErrorDescription: "Ocorreu um erro na expressão regular na linha {line} da palavra mutada {tab}:"
instanceMute: "Instâncias silenciadas"
@@ -908,6 +922,7 @@ followersVisibility: "Visibilidade dos seguidores"
continueThread: "Ver mais desta conversa"
deleteAccountConfirm: "Deseja realmente excluir a conta?"
incorrectPassword: "Senha inválida."
+incorrectTotp: "A senha de uso único está incorreta ou expirou."
voteConfirm: "Deseja confirmar o seu voto em \"{choice}\"?"
hide: "Ocultar"
useDrawerReactionPickerForMobile: "Mostrar em formato de gaveta"
@@ -932,6 +947,9 @@ oneHour: "1 hora"
oneDay: "1 dia"
oneWeek: "1 semana"
oneMonth: "1 mês"
+threeMonths: "3 meses"
+oneYear: "1 ano"
+threeDays: "3 dias"
reflectMayTakeTime: "As mudanças podem demorar a aparecer."
failedToFetchAccountInformation: "Não foi possível obter informações da conta"
rateLimitExceeded: "Taxa limite excedido"
@@ -1072,6 +1090,7 @@ retryAllQueuesConfirmTitle: "Gostaria de tentar novamente agora?"
retryAllQueuesConfirmText: "Isso irá temporariamente aumentar a carga do servidor."
enableChartsForRemoteUser: "Gerar gráficos estatísticos de usuários remotos"
enableChartsForFederatedInstances: "Gerar gráficos estatísticos de instâncias remotas"
+enableStatsForFederatedInstances: "Receber estatísticas de servidores remotos"
showClipButtonInNoteFooter: "Adicionar \"Clip\" ao menu de ação de notas"
reactionsDisplaySize: "Tamanho de exibição das reações"
limitWidthOfReaction: "Limita o comprimento máximo de reações e as exibe em tamanho reduzido"
@@ -1258,7 +1277,49 @@ confirmWhenRevealingSensitiveMedia: "Confirmar ao revelar mídia sensível"
sensitiveMediaRevealConfirm: "Essa mídia pode ser sensível. Deseja revelá-la?"
createdLists: "Listas criadas"
createdAntennas: "Antenas criadas"
+fromX: "De {x}"
+genEmbedCode: "Gerar código de embed"
+noteOfThisUser: "Notas por este usuário"
clipNoteLimitExceeded: "Não é possível adicionar mais notas ao clipe."
+performance: "Desempenho"
+modified: "Modificado"
+discard: "Descartar"
+thereAreNChanges: "Há {n} mudança(s)"
+signinWithPasskey: "Entrar com Passkey"
+unknownWebAuthnKey: "Passkey desconhecida"
+passkeyVerificationFailed: "A verificação com Passkey falhou."
+passkeyVerificationSucceededButPasswordlessLoginDisabled: "A verificação com Passkey teve êxito, mas a entrada sem senha está desabilitada."
+messageToFollower: "Mensagem aos seguidores"
+target: "Alvo"
+testCaptchaWarning: "Essa função é utilizada apenas para testar CAPTCHA. <strong>Não a use num ambiente de produção.</strong>"
+prohibitedWordsForNameOfUser: "Palavras proibidas para nomes de usuário"
+prohibitedWordsForNameOfUserDescription: "Se quaisquer palavras dessa lista forem incluídas no nome de usuário, seu uso será negado. Usuários com privilégios de moderador não serão afetados pela restrição."
+yourNameContainsProhibitedWords: "O seu nome possui palavras proibidas"
+yourNameContainsProhibitedWordsDescription: "Se você deseja utilizar esse nome, entre em contato com o administrador do servidor."
+thisContentsAreMarkedAsSigninRequiredByAuthor: "O autor exige que você esteja cadastrado para ver"
+lockdown: "Lockdown"
+pleaseSelectAccount: "Selecione uma conta"
+availableRoles: "Cargos disponíveis"
+acknowledgeNotesAndEnable: "Ative após compreender as precauções."
+_accountSettings:
+ requireSigninToViewContents: "Exigir cadastro para ver o conteúdo"
+ requireSigninToViewContentsDescription1: "Exigir cadastro para ver todas as notas e outro conteúdo que você criou. Isso previne 'crawlers' de coletar os seus dados."
+ requireSigninToViewContentsDescription2: "Conteúdo não será exibido nas prévias de URL (OGP), incorporado em outras páginas web ou em servidores que não têm suporte a citações."
+ requireSigninToViewContentsDescription3: "Essas restrições podem não ser aplicadas a conteúdo federado de outros servidores."
+ makeNotesFollowersOnlyBefore: "Tornar notas passadas visíveis apenas para seguidores."
+ makeNotesFollowersOnlyBeforeDescription: "Com essa função ativada, apenas seguidores podem ver as notas anteriores à data e hora marcadas. Se isso for desativado, o status de publicação da nota será reestabelecido."
+ makeNotesHiddenBefore: "Tornar notas passadas privadas"
+ makeNotesHiddenBeforeDescription: "Com essa função ativada, apenas você poderá ver as notas anteriores à data e hora marcadas. Se isso for desativado, o status de publicação da nota será reestabelecido."
+ mayNotEffectForFederatedNotes: "Notas federadas a servidores remotos podem não ser afetadas."
+ notesHavePassedSpecifiedPeriod: "Notas que duraram um tempo específico."
+ notesOlderThanSpecifiedDateAndTime: "Notas antes do tempo específico."
+_abuseUserReport:
+ forward: "Encaminhar"
+ forwardDescription: "Encaminhar a denúncia ao servidor remoto como uma conta anônima do sistema."
+ resolve: "Resolver"
+ accept: "Aceitar"
+ reject: "Rejeitar"
+ resolveTutorial: "Se a denúncia for legítima em conteúdo, selecione \"Aceitar\" para marcar o caso como resolvido afirmativamente.\nSe a denúncia for ilegítima em conteúdo, selecione \"Rejeitar\" para marcar o caso como resolvido negativamente."
_delivery:
status: "Estado de entrega"
stop: "Suspenso"
@@ -1393,8 +1454,12 @@ _serverSettings:
fanoutTimelineDescription: "Melhora significativamente a performance do retorno da linha do tempo e reduz o impacto no banco de dados quando habilitado. Em contrapartida, o uso de memória do Redis aumentará. Considere desabilitar em casos de baixa disponibilidade de memória ou instabilidade do servidor."
fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados"
fanoutTimelineDbFallbackDescription: "Quando habilitado, a linha do tempo irá recuar ao banco de dados caso consultas adicionais sejam feitas e ela não estiver em cache. Quando desabilitado, o impacto no servidor será reduzido ao eliminar o recuo, mas limita a quantidade de linhas do tempo que podem ser recebidas."
+ reactionsBufferingDescription: "Quando ativado, o desempenho durante a criação de uma reação será melhorado substancialmente, reduzindo a carga do banco de dados. Porém, a o uso de memória do Redis irá aumentar."
inquiryUrl: "URL de inquérito"
inquiryUrlDescription: "Especifique um URL para um formulário de inquérito para a administração ou uma página web com informações de contato."
+ openRegistration: "Abrir a criação de contas"
+ openRegistrationWarning: "Abrir cadastros contém riscos. É recomendado apenas habilitá-los se houver um sistema de monitoramento contínuo e resolução imediata de problemas."
+ thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Se nenhuma atividade da moderação for detectada por um tempo, essa configuração será desativada para prevenir spam."
_accountMigration:
moveFrom: "Migrar outra conta para essa"
moveFromSub: "Criar um 'alias' a outra conta"
@@ -1726,6 +1791,11 @@ _role:
canSearchNotes: "Permitir a busca de notas"
canUseTranslator: "Uso do tradutor"
avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas"
+ canImportAntennas: "Permitir importação de antenas"
+ canImportBlocking: "Permitir importação de bloqueios"
+ canImportFollowing: "Permitir importação de usuários seguidos"
+ canImportMuting: "Permitir importação de silenciamentos"
+ canImportUserLists: "Permitir importação de listas"
_condition:
roleAssignedTo: "Atribuído a cargos manuais"
isLocal: "Usuário local"
@@ -2109,8 +2179,11 @@ _auth:
permissionAsk: "O aplicativo solicita as seguintes permissões"
pleaseGoBack: "Por favor, volte ao aplicativo"
callback: "Retornando ao aplicativo"
+ accepted: "Acesso permitido"
denied: "Acesso negado"
+ scopeUser: "Operar como o usuário a seguir"
pleaseLogin: "Por favor, entre para autorizar aplicativos."
+ byClickingYouWillBeRedirectedToThisUrl: "Quando o acesso for permitido, você será redirecionado para o seguinte endereço"
_antennaSources:
all: "Todas as notas"
homeTimeline: "Notas de usuários seguidos"
@@ -2219,6 +2292,9 @@ _profile:
changeBanner: "Mudar banner"
verifiedLinkDescription: "Ao inserir um URL que contém um link para essa conta, um ícone de verificação será exibido ao lado do campo"
avatarDecorationMax: "Você pode adicionar até {max} decorações."
+ followedMessage: "Mensagem exibida quando alguém segue você"
+ followedMessageDescription: "Você pode definir uma curta mensagem que será exibida aos usuários que seguirem você."
+ followedMessageDescriptionForLockedAccount: "Se você aceita pedidos de seguidor manualmente, isso será exibido quando você aceitá-los."
_exportOrImport:
allNotes: "Todas as notas"
favoritedNotes: "Notas nos favoritos"
@@ -2281,9 +2357,6 @@ _pages:
newPage: "Criar uma Página"
editPage: "Editar essa Página"
readPage: "Ver a fonte dessa Página"
- created: "Página criada com sucesso"
- updated: "Página atualizada com sucesso"
- deleted: "Página excluída com sucesso"
pageSetting: "Configurações da página"
nameAlreadyExists: "O URL de Página especificado já existe"
invalidNameTitle: "O URL de Página especificado é inválido"
@@ -2357,6 +2430,8 @@ _notification:
renotedBySomeUsers: "{n} usuários repostaram a nota"
followedBySomeUsers: "{n} usuários te seguiram"
flushNotification: "Limpar notificações"
+ exportOfXCompleted: "Exportação de {x} foi concluída"
+ login: "Alguém entrou na conta"
_types:
all: "Todas"
note: "Novas notas"
@@ -2371,7 +2446,9 @@ _notification:
followRequestAccepted: "Aceitou pedidos de seguidor"
roleAssigned: "Cargo dado"
achievementEarned: "Conquista desbloqueada"
+ exportCompleted: "A exportação foi concluída"
login: "Iniciar sessão"
+ test: "Notificação teste"
app: "Notificações de aplicativos conectados"
_actions:
followBack: "te seguiu de volta"
@@ -2437,7 +2514,10 @@ _webhookSettings:
abuseReport: "Quando receber um relatório de abuso"
abuseReportResolved: "Quando relatórios de abuso forem resolvidos "
userCreated: "Quando um usuário é criado"
+ inactiveModeratorsWarning: "Quando moderadores estiverem inativos por um tempo"
+ inactiveModeratorsInvitationOnlyChanged: "Quando um moderador está inativo por um tempo e os cadastros passam a exigir convites"
deleteConfirm: "Você tem certeza de que deseja excluir o Webhook?"
+ testRemarks: "Clique no botão à direita do interruptor para enviar um Webhook de teste com dados fictícios."
_abuseReport:
_notificationRecipient:
createRecipient: "Adicionar destinatário para relatórios de abuso"
@@ -2481,6 +2561,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "Arquivo marcado como sensível"
unmarkSensitiveDriveFile: "Arquivo desmarcado como sensível"
resolveAbuseReport: "Relatório resolvido"
+ forwardAbuseReport: "Denúncia encaminhada"
+ updateAbuseReportNote: "Nota de moderação da denúncia atualizada"
createInvitation: "Convite gerado"
createAd: "Propaganda criada"
deleteAd: "Propaganda excluída"
@@ -2636,3 +2718,44 @@ _contextMenu:
app: "Aplicativo"
appWithShift: "Aplicativo com a tecla shift"
native: "Nativo"
+_embedCodeGen:
+ title: "Personalizar código do embed"
+ header: "Exibir cabeçalho"
+ autoload: "Carregar mais automaticamente (obsoleto)"
+ maxHeight: "Altura máxima"
+ maxHeightDescription: "Colocar em 0 desabilita a altura máxima. Especifique um valor para prevenir uma expansão vertical contínua."
+ maxHeightWarn: "O limite de altura máxima está desabilitado (0). Se isso não for intencional, insira um valor para a altura máxima."
+ previewIsNotActual: "A exibição difere do embed original porque ela excede o tamanho da tela de prévia."
+ rounded: "Tornar arredondado"
+ border: "Adicionar uma borda ao quadro externo"
+ applyToPreview: "Aplicar para a prévia"
+ generateCode: "Gerar código de embed"
+ codeGenerated: "O código foi gerado"
+ codeGeneratedDescription: "Coloque o código no seu website para incorporar o conteúdo."
+_selfXssPrevention:
+ warning: "AVISO"
+ title: "\"Cole algo nessa tela\" é uma fraude"
+ description1: "Se você colar algo aqui, um usuário malicioso pode sabotar a sua conta ou roubar informações pessoais."
+ description2: "Se você não entender exatamente o que está colando, %cpare agora e feche essa janela."
+ description3: "Para mais informação, clique no link. {link}"
+_followRequest:
+ recieved: "Aplicação recebida"
+ sent: "Aplicação enviada"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "Não foi possível se comunicar com o servidor"
+ description: "Comunicação com esse servidor pode ter sido desabilitada ou o servidor pode ter sido bloqueado.\nPor favor, entre em contato com o administrador do servidor."
+ _uriInvalid:
+ title: "Endereço inválido"
+ description: "Há um problema com o endereço inserido. Por favor, confira se você não inseriu caracteres inválidos."
+ _requestFailed:
+ title: "Solicitação falhou"
+ description: "Comunicação com esse servidor falhou. O servidor pode estar inativo. Além disso, confira se você não inseriu um endereço inválido ou inexistente."
+ _responseInvalid:
+ title: "Resposta inválida"
+ description: "Foi possível comunicar com o servidor, porém os dados obtidos foram incorretos."
+ _responseInvalidIdHostNotMatch:
+ description: "O domínio do endereço inserido difere do domínio do endereço final. Se você estiver pesquisando por um servidor de terceiros, tente buscar novamente com um endereço que pode ser obtido através do servidor original."
+ _noSuchObject:
+ title: "Não encontrado"
+ description: "O recurso solicitado não foi encontrado, confira o endereço."
diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml
index 71dc1dc94c..07f4c98d96 100644
--- a/locales/ro-RO.yml
+++ b/locales/ro-RO.yml
@@ -733,3 +733,6 @@ _moderationLogTypes:
resetPassword: "Resetează parola"
_reversi:
total: "Total"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Nu a fost găsit"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 537e99036c..7ed41a9c47 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -18,7 +18,7 @@ gotIt: "Ясно!"
cancel: "Отмена"
noThankYou: "Нет, спасибо"
enterUsername: "Введите имя пользователя"
-renotedBy: "{user} репостнул(а)"
+renotedBy: "{user} делает репост"
noNotes: "Нет ни одной заметки"
noNotifications: "Нет уведомлений"
instance: "Экземпляр"
@@ -1063,7 +1063,7 @@ hiddenTags: "Скрытые хештеги"
notesSearchNotAvailable: "Поиск заметок недоступен"
license: "Лицензия"
unfavoriteConfirm: "Удалить избранное?"
-myClips: "Мои клипы"
+myClips: "Мои подборки"
drivecleaner: "Очиститель дисков"
retryAllQueuesNow: "Повторить все очереди сейчас"
retryAllQueuesConfirmTitle: "Хотите попробовать ещё раз?"
@@ -1105,16 +1105,18 @@ preservedUsernames: "Зарезервированные имена пользо
preservedUsernamesDescription: "Перечислите зарезервированные имена пользователей, отделяя их строками. Они станут недоступны при создании учётной записи. Это ограничение не применяется при создании учётной записи администраторами. Также, уже существующие учётные записи останутся без изменений."
createNoteFromTheFile: "Создать заметку из этого файла"
archive: "Архив"
+unarchive: "Разархивировать"
channelArchiveConfirmTitle: "Переместить {name} в архив?"
channelArchiveConfirmDescription: "Архивированные каналы перестанут отображаться в списке каналов или результатах поиска. В них также нельзя будет добавлять новые записи."
thisChannelArchived: "Этот канал находится в архиве."
displayOfNote: "Отображение заметок"
initialAccountSetting: "Настройка профиля"
-youFollowing: "Подписки"
+youFollowing: "Вы подписаны"
preventAiLearning: "Отказаться от использования в машинном обучении (Генеративный ИИ)"
preventAiLearningDescription: "Запросить краулеров не использовать опубликованный текст или изображения и т.д. для машинного обучения (Прогнозирующий / Генеративный ИИ) датасетов. Это достигается путём добавления \"noai\" HTTP-заголовка в ответ на соответствующий контент. Полного предотвращения через этот заголовок не избежать, так как он может быть просто проигнорирован."
options: "Настройки ролей"
specifyUser: "Указанный пользователь"
+lookupConfirm: "Хотите узнать?"
openTagPageConfirm: "Открыть страницу этого хештега?"
specifyHost: "Указать сайт"
failedToPreviewUrl: "Предварительный просмотр недоступен"
@@ -1178,6 +1180,7 @@ keepOriginalFilename: "Сохранять исходное имя файла"
keepOriginalFilenameDescription: "Если вы выключите данную настройку, имена файлов будут автоматически заменены случайной строкой при загрузке."
alwaysConfirmFollow: "Всегда подтверждать подписку"
inquiry: "Связаться"
+messageToFollower: "Сообщение подписчикам"
_delivery:
stop: "Заморожено"
_type:
@@ -1504,6 +1507,7 @@ _role:
rateLimitFactor: "Ограничение активности"
descriptionOfRateLimitFactor: "Меньшее значение — слабые ограничения, большее — сильные"
canHideAds: "Может скрыть рекламу"
+ canImportFollowing: "Можно импортировать подписчиков"
_condition:
isLocal: "Местный"
isRemote: "Неместный"
@@ -1972,9 +1976,6 @@ _pages:
newPage: "Создать страницу"
editPage: "Править страницу"
readPage: "Читать страницу"
- created: "Страница успешно создана."
- updated: "Страница успешно обновлена."
- deleted: "Страница успешно удалена."
pageSetting: "Настройки страницы"
nameAlreadyExists: "Указанный адрес страницы уже существует."
invalidNameTitle: "Указанный адрес страницы недопустим."
@@ -2143,3 +2144,6 @@ _hemisphere:
caption: "Используется для некоторых настроек клиента для определения сезона."
_reversi:
total: "Всего"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Не найдено"
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index f3f43ee6a6..521d172671 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -1332,9 +1332,6 @@ _pages:
newPage: "Vytvoriť novú stránku"
editPage: "Upraviť túto stránku"
readPage: "Zobrazenie zdroja aktívne"
- created: "Stránka úspešne vytvorená"
- updated: "Stránka úspešne upravená"
- deleted: "Stránka úspešne odstránená"
pageSetting: "Nastavenia stránky"
nameAlreadyExists: "Zadaná URL stránku už existuje"
invalidNameTitle: "Zadaná URL stránku je nesprávna"
@@ -1449,3 +1446,6 @@ _moderationLogTypes:
resetPassword: "Resetovať heslo"
_reversi:
total: "Celkom"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Nenájdené"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index c725282d50..ec83ba888c 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -2331,9 +2331,6 @@ _pages:
newPage: "สร้างหน้าเพจใหม่"
editPage: "แก้ไขหน้าเพจ"
readPage: "กำลังดูแหล่งที่มาของเพจนี้"
- created: "สร้างหน้าเพจสำเร็จเรียบร้อยแล้ว"
- updated: "แก้ไขหน้าเพจสำเร็จเรียบร้อยแล้ว"
- deleted: "ลบหน้าเพจสำเร็จเรียบร้อยแล้ว"
pageSetting: "การตั้งค่าหน้าเพจ"
nameAlreadyExists: "URL ของหน้าที่ระบุนั้นมีอยู่แล้ว"
invalidNameTitle: "URL ของหน้าที่ระบุนั้นไม่ถูกต้อง"
@@ -2709,3 +2706,6 @@ _embedCodeGen:
generateCode: "สร้างโค้ดสำหรับการฝัง"
codeGenerated: "รหัสถูกสร้างขึ้นแล้ว"
codeGeneratedDescription: "นำโค้ดที่สร้างแล้วไปวางในเว็บไซต์ของคุณเพื่อฝังเนื้อหา"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "ไม่พบหน้าที่ต้องการ"
diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml
index 69892fedc8..2c63f15aa2 100644
--- a/locales/tr-TR.yml
+++ b/locales/tr-TR.yml
@@ -8,6 +8,7 @@ search: "Arama"
notifications: "Bildirim"
username: "Kullanıcı Adı"
password: "Şifre"
+initialPasswordForSetup: ""
forgotPassword: "şifremi unuttum"
fetchingAsApObject: "從聯邦宇宙取得中..."
ok: "TAMAM"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 1b21854650..a83ad80683 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -1513,9 +1513,6 @@ _pages:
newPage: "Створити сторінку"
editPage: "Редагувати сторінку"
readPage: "Перегляд вихідного коду"
- created: "Сторінка успішно створена."
- updated: "Сторінка успішно оновлена."
- deleted: "Сторінку видалено"
pageSetting: "Налаштування сторінки"
nameAlreadyExists: "Вказана адреса сторінки вже існує."
invalidNameTitle: "Вказана адреса сторінки неприпустима."
@@ -1624,3 +1621,6 @@ _moderationLogTypes:
resetPassword: "Скинути пароль"
_reversi:
total: "Всього"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Не знайдено"
diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml
index 051a4ae6c5..6015492b92 100644
--- a/locales/uz-UZ.yml
+++ b/locales/uz-UZ.yml
@@ -1004,9 +1004,6 @@ _play:
_pages:
newPage: "Yangi Sahifa yaratish"
editPage: "Ushbu Sahifani tahrirlash"
- created: "Sahifa muvaffaqiyatli yaratildi"
- updated: "Sahifa muvaffaqiyatli tahrirlandi"
- deleted: "Sahifa muvaffaqiyatli o'chirildi"
pageSetting: "Sahifa sozlamalari"
nameAlreadyExists: "Ko'rsatilgan Sahifa URL'i allaqachon mavjud"
invalidNameTitle: "Ko'rsatilgan Sahifa URL'i yaroqsiz"
@@ -1094,3 +1091,6 @@ _moderationLogTypes:
resetPassword: "Parolni tiklash"
_reversi:
total: "Jami"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Topilmadi"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index 24faa4b94c..e6a9418126 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -1802,9 +1802,6 @@ _pages:
newPage: "Tạo Trang mới"
editPage: "Sửa Trang này"
readPage: "Xem mã nguồn Trang này"
- created: "Trang đã được tạo thành công"
- updated: "Trang đã được cập nhật thành công"
- deleted: "Trang đã được xóa thành công"
pageSetting: "Cài đặt trang"
nameAlreadyExists: "URL Trang đã tồn tại"
invalidNameTitle: "URL Trang không hợp lệ"
@@ -1930,3 +1927,6 @@ _moderationLogTypes:
createInvitation: "Tạo lời mời"
_reversi:
total: "Tổng cộng"
+_remoteLookupErrors:
+ _noSuchObject:
+ title: "Không tìm thấy"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index e6232070d7..f4df425af4 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -5,6 +5,7 @@ introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客
poweredByMisskeyDescription: "{name} 是开源平台 <b>Misskey</b> 的服务器之一。"
monthAndDay: "{month}月 {day}日"
search: "搜索"
+reset: "重置"
notifications: "通知"
username: "用户名"
password: "密码"
@@ -48,6 +49,7 @@ pin: "置顶"
unpin: "取消置顶"
copyContent: "复制内容"
copyLink: "复制链接"
+copyRemoteLink: "复制远程链接"
copyLinkRenote: "复制转帖链接"
delete: "删除"
deleteAndEdit: "删除并编辑"
@@ -142,15 +144,15 @@ markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "输入文件名"
mute: "屏蔽"
-unmute: "解除静音"
+unmute: "取消隐藏"
renoteMute: "隐藏转帖"
renoteUnmute: "解除隐藏转帖"
-block: "拉黑"
-unblock: "取消拉黑"
+block: "屏蔽"
+unblock: "取消屏蔽"
suspend: "冻结"
unsuspend: "解除冻结"
-blockConfirm: "确定要拉黑吗?"
-unblockConfirm: "确定要解除拉黑吗?"
+blockConfirm: "确定要屏蔽吗?"
+unblockConfirm: "确定要取消屏蔽吗?"
suspendConfirm: "要冻结吗?"
unsuspendConfirm: "要解除冻结吗?"
selectList: "选择列表"
@@ -195,7 +197,7 @@ setWallpaper: "设置壁纸"
removeWallpaper: "移除壁纸"
searchWith: "搜索:{q}"
youHaveNoLists: "列表为空"
-followConfirm: "你确定要关注 {name} 吗?"
+followConfirm: "确定要关注 {name} 吗?"
proxyAccount: "代理账户"
proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。"
host: "主机名"
@@ -229,10 +231,10 @@ disk: "存储"
instanceInfo: "服务器信息"
statistics: "统计"
clearQueue: "清除队列"
-clearQueueConfirmTitle: "确定清除队列?"
+clearQueueConfirmTitle: "确定要清除队列吗?"
clearQueueConfirmText: "未送达的帖子将不会被投递。 通常无需执行此操作。"
clearCachedFiles: "清除缓存"
-clearCachedFilesConfirm: "确定要清除所有缓存的远程文件?"
+clearCachedFilesConfirm: "确定要清除所有缓存的远程文件吗?"
blockedInstances: "被屏蔽的服务器"
blockedInstancesDescription: "设定要屏蔽的服务器,以换行分隔。被屏蔽的服务器将无法与本服务器进行交换通讯。子域名也同样会被屏蔽。"
silencedInstances: "被静音的服务器"
@@ -246,7 +248,7 @@ mutedUsers: "已隐藏用户"
blockedUsers: "已屏蔽的用户"
noUsers: "无用户"
editProfile: "编辑资料"
-noteDeleteConfirm: "要删除该帖子吗?"
+noteDeleteConfirm: "确定要删除该帖子吗?"
pinLimitExceeded: "无法置顶更多了"
intro: "Misskey 的部署结束啦!创建管理员账号吧!"
done: "完成"
@@ -257,7 +259,7 @@ defaultValueIs: "默认值: {value}"
noCustomEmojis: "没有自定义表情符号"
noJobs: "没有任务"
federating: "联合中"
-blocked: "已拉黑"
+blocked: "已屏蔽"
suspended: "停止投递"
all: "全部"
subscribing: "已订阅"
@@ -566,7 +568,7 @@ objectStorageRegionDesc: "指定一个可用区,例如“xx-east-1”。 如
objectStorageUseSSL: "使用 SSL"
objectStorageUseSSLDesc: "如果不使用 https 进行 API 连接,请关闭。"
objectStorageUseProxy: "使用代理"
-objectStorageUseProxyDesc: "如果您不使用代理进行 API 连接,请将其关闭。"
+objectStorageUseProxyDesc: "如果不使用代理进行 API 连接,请关闭。"
objectStorageSetPublicRead: "上传时设置为 public-read"
s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。"
serverLogs: "服务器日志"
@@ -683,12 +685,16 @@ emptyToDisableSmtpAuth: "用户名和密码留空可以禁用 SMTP 验证"
smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS"
smtpSecureInfo: "使用 STARTTLS 时关闭。"
testEmail: "邮件发送测试"
-wordMute: "隐藏文字"
-hardWordMute: "屏蔽关键词"
+wordMute: "隐藏关键词"
+wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。"
+hardWordMute: "隐藏硬关键词"
+showMutedWord: "显示已隐藏的关键词"
+hardWordMuteDescription: "隐藏包含指定关键词的帖子。与隐藏关键词不同,帖子将完全不会显示。"
regexpError: "正则表达式错误"
-regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:"
+regexpErrorDescription: "{tab} 隐藏文字的第 {line} 行的正则表达式有错误:"
instanceMute: "已隐藏的服务器"
userSaysSomething: "{name} 说了什么,但是被屏蔽词过滤了"
+userSaysSomethingAbout: "{name} 说了关于「{word}」的什么"
makeActive: "启用"
display: "显示"
copy: "复制"
@@ -759,7 +765,7 @@ driveFilesCount: "网盘的文件数"
driveUsage: "网盘的空间用量"
noCrawle: "要求搜索引擎不索引该用户"
noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。"
-lockedAccountInfo: "即使启用该功能,只要您不将帖子可见范围设置为“仅关注者”,任何人都还是可以看到您的帖子。"
+lockedAccountInfo: "即使启用该功能,只要帖子可见范围不是「仅关注者」,任何人都可以看到您的帖子。"
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
disableShowingAnimatedImages: "不播放动画"
@@ -846,7 +852,7 @@ active: "活动"
offline: "离线"
notRecommended: "不推荐"
botProtection: "Bot防御"
-instanceBlocking: "被阻拦的服务器"
+instanceBlocking: "屏蔽/静音的服务器"
selectAccount: "选择账户"
switchAccount: "切换账户"
enabled: "已启用"
@@ -1301,6 +1307,8 @@ lockdown: "锁定"
pleaseSelectAccount: "请选择帐户"
availableRoles: "可用角色"
acknowledgeNotesAndEnable: "理解注意事项后再开启。"
+federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。"
+federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。"
_accountSettings:
requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
@@ -1319,7 +1327,7 @@ _abuseUserReport:
resolve: "解决"
accept: "确认"
reject: "拒绝"
- resolveTutorial: "如果举报内容有理且已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果举报内容站不住脚,选择「拒绝」将案件以否定的态度标记为已解决。"
+ resolveTutorial: "如果认可举报并已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果不认可举报,选择「拒绝」将案件以否定的态度标记为已解决。"
_delivery:
status: "投递状态"
stop: "停止投递"
@@ -1468,7 +1476,7 @@ _accountMigration:
moveTo: "把这个账户迁移到新的账户"
moveToLabel: "迁移后的账户"
moveCannotBeUndone: "一旦迁移账户,就无法撤销。"
- moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n屏蔽列表也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)"
+ moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n列表、隐藏、屏蔽也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)"
moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后,请像这样输入目标账户:@username@server.example.com"
startMigration: "迁移"
migrationConfirm: "确定要把此账户迁移到 {account} 吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。"
@@ -1688,7 +1696,7 @@ _achievements:
title: "超高校级的幸运"
description: "每 10 秒有 0.005% 的概率自动获得"
_setNameToSyuilo:
- title: "像神一样呐"
+ title: "上帝情结"
description: "将名称设定为 syuilo"
_passedSinceAccountCreated1:
title: "一周年"
@@ -1794,7 +1802,7 @@ _role:
canImportAntennas: "允许导入天线"
canImportBlocking: "允许导入屏蔽列表"
canImportFollowing: "允许导入关注列表"
- canImportMuting: "允许导入屏蔽列表"
+ canImportMuting: "允许导入隐藏列表"
canImportUserLists: "允许导入用户列表"
_condition:
roleAssignedTo: "已分配给手动角色"
@@ -1948,7 +1956,7 @@ _wordMute:
_instanceMute:
instanceMuteDescription: "隐藏服务器中的所有帖子和转帖,包括这些服务器上的用户回复。"
instanceMuteDescription2: "一行一个"
- title: "隐藏服务器已设置的帖子。"
+ title: "下面实例中的帖子将被隐藏。"
heading: "已隐藏的服务器"
_theme:
explore: "寻找主题"
@@ -2069,12 +2077,12 @@ _2fa:
step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
securityKeyNotSupported: "您的浏览器不支持安全密钥。"
registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器。"
- securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。"
+ securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 以及 Passkey 等。"
registerSecurityKey: "注册安全密钥或 Passkey"
securityKeyName: "输入密钥名称"
tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或 Passkey。"
removeKey: "删除安全密钥"
- removeKeyConfirm: "您确定要删除 {name} 吗?"
+ removeKeyConfirm: "确定要删除 {name} 吗?"
whyTOTPOnlyRenew: "当注册了安全密钥时,无法取消使用验证器。"
renewTOTP: "重置验证器"
renewTOTPConfirm: "当前验证器的验证码及备用代码已失效"
@@ -2282,7 +2290,7 @@ _profile:
name: "昵称"
username: "用户名"
description: "个人简介"
- youCanIncludeHashtags: "你可以在个人简介中包含一些#标签。"
+ youCanIncludeHashtags: "可以在个人简介中包含 #标签。"
metadata: "附加信息"
metadataEdit: "附加信息编辑"
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
@@ -2357,9 +2365,6 @@ _pages:
newPage: "创建页面"
editPage: "编辑页面"
readPage: "查看页面"
- created: "页面已创建"
- updated: "页面已更新"
- deleted: "该页面已被删除"
pageSetting: "页面设置"
nameAlreadyExists: "该页面 URL 已存在"
invalidNameTitle: "无效的页面 URL"
@@ -2386,7 +2391,7 @@ _pages:
fontSansSerif: "无衬线字体"
eyeCatchingImageSet: "设置封面图片"
eyeCatchingImageRemove: "删除封面图片"
- chooseBlock: "添加块"
+ chooseBlock: "添加内容块"
enterSectionTitle: "输入会话标题"
selectType: "选择类型"
contentBlocks: "内容"
@@ -2398,8 +2403,8 @@ _pages:
section: "章节"
image: "图片"
button: "按钮"
- dynamic: "动态区块"
- dynamicDescription: "这个区块已经废弃。以后请使用{play}。"
+ dynamic: "动态内容块"
+ dynamicDescription: "这个内容块已经废弃。以后请使用{play}。"
note: "嵌入的帖子"
_note:
id: "帖子 ID"
@@ -2433,7 +2438,7 @@ _notification:
renotedBySomeUsers: "{n} 人转发了"
followedBySomeUsers: "被 {n} 人关注"
flushNotification: "重置通知历史"
- exportOfXCompleted: "已完成 {x} 个导出"
+ exportOfXCompleted: "已完成 {x} 的导出"
login: "有新的登录"
_types:
all: "全部"
@@ -2721,6 +2726,66 @@ _contextMenu:
app: "应用"
appWithShift: "Shift 键应用"
native: "浏览器的用户界面"
+_gridComponent:
+ _error:
+ requiredValue: "此值为必填项"
+ columnTypeNotSupport: "正则表达式验证仅支持 type:text 列。"
+ patternNotMatch: "此值与 {pattern} 的模式不一致"
+ notUnique: "此值必须唯一"
+_roleSelectDialog:
+ notSelected: "未选中"
+_customEmojisManager:
+ _gridCommon:
+ copySelectionRows: "复制所选行"
+ copySelectionRanges: "复制所选范围"
+ deleteSelectionRows: "删除所选行"
+ deleteSelectionRanges: "删除所选范围的行"
+ searchSettings: "搜索设置"
+ searchSettingCaption: "设置详细的搜索条件。"
+ searchLimit: "显示项目数"
+ sortOrder: "排序方式"
+ registrationLogs: "注册日志"
+ registrationLogsCaption: "将显示更新和删除表情符号的日志。执行更新或删除操作,又或者更改或重新加载页面时会消失。"
+ alertEmojisRegisterFailedDescription: "更新或删除表情符号失败。详情请确认注册日志。"
+ _logs:
+ showSuccessLogSwitch: "显示成功日志"
+ failureLogNothing: "没有失败日志。"
+ logNothing: "没有日志"
+ _remote:
+ selectionRowDetail: "所选行的详细信息"
+ importSelectionRows: "导入所选行"
+ importSelectionRangesRows: "导入所选范围的行"
+ importEmojisButton: "导入已选择的表情符号"
+ confirmImportEmojisTitle: "导入表情符号"
+ confirmImportEmojisDescription: "是否导入从远程服务器接收的 {count} 个表情符号?请密切关注表情符号的许可协议。"
+ _local:
+ tabTitleList: "已注册的表情符号列表"
+ tabTitleRegister: "注册表情符号"
+ _list:
+ emojisNothing: "没有已注册的表情符号。"
+ markAsDeleteTargetRows: "将所选行标记为删除对象"
+ markAsDeleteTargetRanges: "将所选范围的行标记为删除对象"
+ alertUpdateEmojisNothingDescription: "没有已更改的表情符号。"
+ alertDeleteEmojisNothingDescription: "没有被标记为删除对象的表情符号。"
+ confirmMovePage: "要离开此页吗?"
+ confirmChangeView: "要更改显示吗?"
+ confirmUpdateEmojisDescription: "要更新 {count} 个表情符号吗?"
+ confirmDeleteEmojisDescription: "要删除已选择的 {count} 个表情符号吗?"
+ confirmResetDescription: "至今为止所做的所有修改都将被重置。"
+ confirmMovePageDesciption: "此页面上的表情符号已更改。\n若不保存就离开此页,此页面上所有的更改都将丢失。"
+ dialogSelectRoleTitle: "按角色搜索表情符号"
+ _register:
+ uploadSettingTitle: "上传设置"
+ uploadSettingDescription: "可以在此页面设置上传表情符号时的行为。"
+ directoryToCategoryLabel: "目录名请输入「category」"
+ directoryToCategoryCaption: "拖放目录时,目录名请输入「category」"
+ emojiInputAreaCaption: "请使用其中一种方法选择要注册的表情符号。"
+ emojiInputAreaList1: "在此区域内拖放图像文件或者目录"
+ emojiInputAreaList2: "单击此链接以从电脑中选择"
+ emojiInputAreaList3: "单击此链接以从网盘中选择"
+ confirmRegisterEmojisDescription: "要将列表内显示的表情符号替换为新的自定义表情符号吗?(为降低服务器负载,一次操作最多只能注册 {count} 个表情符号)"
+ confirmClearEmojisDescription: "要放弃编辑并将列表内表示的表情符号清空吗?"
+ confirmUploadEmojisDescription: "要将拖放的 {count} 个文件上传到网盘上吗?"
_embedCodeGen:
title: "自定义嵌入代码"
header: "显示标题"
@@ -2744,3 +2809,34 @@ _selfXssPrevention:
_followRequest:
recieved: "已收到申请"
sent: "已发送申请"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "无法与此服务器通信"
+ description: "与此服务器的通信可能被禁用,又或者是屏蔽了此服务器或被此服务器屏蔽了。\n请联系服务器的管理者。"
+ _uriInvalid:
+ title: "URI 有误"
+ description: "输入的 URI 有问题。请确认是否输入了 URI 中无法使用的字符。"
+ _requestFailed:
+ title: "请求失败"
+ description: "与该服务器的通信失败。对面服务器可能不可用。另外,请确认是否输入了无效或不存在的 URI。"
+ _responseInvalid:
+ title: "响应无效"
+ description: "成功与此服务器通信,但返回的数据无效。"
+ _responseInvalidIdHostNotMatch:
+ description: "输入 URI 的域名和最终取得的 URI 的域名不同。如果是通过第三方服务器获取远程内容,请使用可以从原始服务器获取内容的 URI 再试一次。"
+ _noSuchObject:
+ title: "未找到"
+ description: "未找到请求的资源。请再次检查 URI。"
+_captcha:
+ verify: "请通过 CAPTCHA 验证"
+ testSiteKeyMessage: "输入测试用的网站密钥及私密密钥后可以生成预览并检查,\n详情请看以下页面。"
+ _error:
+ _requestFailed:
+ title: "请求 CAPTCHA 失败"
+ text: "请稍后再试,又或者再检查一次设置。"
+ _verificationFailed:
+ title: "验证 CAPTCHA 失败"
+ text: "请再次确认设置是否正确。"
+ _unknown:
+ title: "CAPTCHA 错误"
+ text: "发生意外错误。"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index d4ffb28c76..466e3cc1d8 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -5,6 +5,7 @@ introMisskey: "歡迎!Misskey 是一個開放原始碼且去中心化的社群
poweredByMisskeyDescription: "{name}是開放原始碼平臺 <b>Misskey</b> 的伺服器之一。"
monthAndDay: "{month} 月 {day} 日"
search: "搜尋"
+reset: "重設"
notifications: "通知"
username: "使用者名稱"
password: "密碼"
@@ -48,6 +49,7 @@ pin: "置頂"
unpin: "取消置頂"
copyContent: "複製內容"
copyLink: "複製連結"
+copyRemoteLink: "複製遠端的連結"
copyLinkRenote: "複製轉發的連結"
delete: "刪除"
deleteAndEdit: "刪除並編輯"
@@ -230,7 +232,7 @@ instanceInfo: "伺服器資訊"
statistics: "統計"
clearQueue: "清除佇列"
clearQueueConfirmTitle: "確定要清除佇列嗎?"
-clearQueueConfirmText: "未發佈的貼文將不會發佈。您通常不需要確認。"
+clearQueueConfirmText: "未成功發佈的貼文將不會再嘗試發佈。通常不需要進行這項操作。"
clearCachedFiles: "清除快取資料"
clearCachedFilesConfirm: "確定要清除所有遠端暫存資料嗎?"
blockedInstances: "已封鎖的伺服器"
@@ -291,8 +293,8 @@ messaging: "聊天"
upload: "上傳"
keepOriginalUploading: "保留原圖"
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成適用於網路傳送的版本。"
-fromDrive: "從雲端空間"
-fromUrl: "從 URL"
+fromDrive: "從雲端空間中選擇"
+fromUrl: "從 URL 上傳"
uploadFromUrl: "從網址上傳"
uploadFromUrlDescription: "您要上傳的檔案網址"
uploadFromUrlRequested: "已請求上傳"
@@ -324,7 +326,7 @@ light: "淺色"
dark: "深色"
lightThemes: "淺色佈景主題"
darkThemes: "深色佈景主題"
-syncDeviceDarkMode: "與設備的深色模式同步"
+syncDeviceDarkMode: "與裝置的深色模式同步"
drive: "雲端硬碟"
fileName: "檔案名稱"
selectFile: "選擇檔案"
@@ -684,11 +686,15 @@ smtpSecure: "在 SMTP 連接中使用隱式 SSL/TLS"
smtpSecureInfo: "使用 STARTTLS 時關閉。"
testEmail: "測試郵件發送"
wordMute: "被靜音的文字"
+wordMuteDescription: "將包含指定語句的貼文最小化。 點擊最小化的貼文即可顯示。"
hardWordMute: "硬文字靜音"
+showMutedWord: "顯示靜音字"
+hardWordMuteDescription: "隱藏含有指定語句的貼文。 與詞彙靜音不同的是,貼文將完全隱藏不見。"
regexpError: "正規表達式錯誤"
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
instanceMute: "被靜音的實例"
userSaysSomething: "{name}說了什麼"
+userSaysSomethingAbout: "{name} 說了一些關於「{word}」的話"
makeActive: "啟用"
display: "檢視"
copy: "複製"
@@ -764,7 +770,7 @@ alwaysMarkSensitive: "預設標記檔案為敏感內容"
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
disableShowingAnimatedImages: "不播放動態圖檔"
highlightSensitiveMedia: "強調敏感標記"
-verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。"
+verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的連結以完成驗證。"
notSet: "未設定"
emailVerified: "已成功驗證您的電子郵件地址"
noteFavoritesCount: "我的最愛貼文的數目"
@@ -821,7 +827,7 @@ apply: "套用"
receiveAnnouncementFromInstance: "接收來自伺服器的通知"
emailNotification: "郵件通知"
publish: "發布"
-inChannelSearch: "頻道内搜尋"
+inChannelSearch: "頻道內搜尋"
useReactionPickerForContextMenu: "點擊右鍵開啟反應選擇器"
typingUsers: "{users}輸入中"
jumpToSpecifiedDate: "跳轉到特定日期"
@@ -925,7 +931,7 @@ incorrectPassword: "密碼錯誤。"
incorrectTotp: "一次性密碼錯誤,或者已過期。"
voteConfirm: "確定投給「{choice}」?"
hide: "隱藏"
-useDrawerReactionPickerForMobile: "在移動設備上使用抽屜顯示"
+useDrawerReactionPickerForMobile: "在行動裝置上使用抽屜顯示"
welcomeBackWithName: "歡迎回來,{name}"
clickToFinishEmailVerification: "點擊 [{ok}] 完成電子郵件地址認證。"
overridedDeviceKind: "裝置類型"
@@ -1006,7 +1012,7 @@ unsubscribePushNotification: "停用推播通知"
pushNotificationAlreadySubscribed: "推播通知啟用中"
pushNotificationNotSupported: "瀏覽器或伺服器不支援推播通知"
sendPushNotificationReadMessage: "如果已閱讀通知與訊息,就刪除推播通知"
-sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通知將立刻顯示。可能會更消耗裝置電池。"
+sendPushNotificationReadMessageCaption: "可能會導致裝置的電池消耗量增加。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "復原"
@@ -1175,20 +1181,20 @@ used: "已使用"
expired: "過期"
doYouAgree: "你同意嗎?"
beSureToReadThisAsItIsImportant: "重要,請務必閱讀。"
-iHaveReadXCarefullyAndAgree: "我已仔細閱讀並同意「{x}」的内容。"
+iHaveReadXCarefullyAndAgree: "我已仔細閱讀並同意「{x}」的內容。"
dialog: "對話方塊"
icon: "圖示"
forYou: "給您"
currentAnnouncements: "最新公告"
pastAnnouncements: "歷史公告"
youHaveUnreadAnnouncements: "有未讀的公告。"
-useSecurityKey: "請按照瀏覽器或設備上的說明使用安全金鑰或 Passkey。"
+useSecurityKey: "請按照瀏覽器或裝置上的說明來使用安全金鑰或 Passkey。"
replies: "回覆"
renotes: "轉發"
loadReplies: "閱覽回覆"
loadConversation: "閱覽對話"
pinnedList: "已置頂的清單"
-keepScreenOn: "保持設備螢幕開啟"
+keepScreenOn: "保持裝置螢幕開啟"
verifiedLink: "已驗證連結"
notifyNotes: "開啟貼文通知"
unnotifyNotes: "關閉貼文通知"
@@ -1243,7 +1249,7 @@ overwriteContentConfirm: "確定要覆蓋目前的內容嗎?"
seasonalScreenEffect: "隨季節變換畫面的呈現"
decorate: "設置頭像裝飾"
addMfmFunction: "插入 MFM 功能語法"
-enableQuickAddMfmFunction: "顯示高級 MFM 選擇器"
+enableQuickAddMfmFunction: "顯示進階 MFM 選擇器"
bubbleGame: "氣泡遊戲"
sfx: "音效"
soundWillBePlayed: "將播放音效"
@@ -1270,7 +1276,7 @@ useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊"
keepOriginalFilename: "保留原始檔名"
keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。"
noDescription: "沒有說明文字"
-alwaysConfirmFollow: "跟隨時總是確認"
+alwaysConfirmFollow: "追隨時總是確認"
inquiry: "聯絡我們"
tryAgain: "請再試一次。"
confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認"
@@ -1301,6 +1307,8 @@ lockdown: "鎖定"
pleaseSelectAccount: "請選擇帳戶"
availableRoles: "可用角色"
acknowledgeNotesAndEnable: "了解注意事項後再開啟。"
+federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。"
+federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。"
_accountSettings:
requireSigninToViewContents: "須登入以顯示內容"
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
@@ -1366,7 +1374,7 @@ _initialAccountSetting:
theseSettingsCanEditLater: "這裡的設定可以在之後變更。"
youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。"
followUsers: "為了構築時間軸,試著追隨您感興趣的使用者吧。"
- pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。"
+ pushNotificationDescription: "啟用推送通知後,就可以在裝置上接收來自{name}的通知了。"
initialAccountSettingCompleted: "初始設定完成了!"
haveFun: "盡情享受{name}吧!"
youCanContinueTutorial: "您可以繼續學習如何使用{name}(Misskey),也可以就此打住,立即開始使用。"
@@ -1386,12 +1394,12 @@ _initialTutorial:
description: "在Misskey上發布的內容稱為「貼文」。貼文在時間軸上按時間順序排列,並即時更新。"
reply: "您可以回覆貼文,並像討論串一樣繼續對話。"
renote: "您可以將此貼文分享到自己的時間軸。您也可以在引用時添加文字。"
- reaction: "您可以添加反應。詳細資訊將在下一頁進行說明。"
+ reaction: "您可以加入反應。詳細資訊將在下一頁進行說明。"
menu: "可執行各種操作,如查看貼文詳細資訊和複製連結。"
_reaction:
title: "什麼是反應?"
- description: "您可以在貼文中添加「反應」。您可以使用反應輕鬆隨意地表達「最愛/大心」所無法傳達的細微差別。"
- letsTryReacting: "可以透過點擊貼文上的「+」按鈕來添加反應。請嘗試在此範例貼文添加反應!"
+ description: "您可以在貼文中加上「反應」。有些用「最愛/大心」無法傳達的感想,可以用反應輕鬆地表達出來。"
+ letsTryReacting: "按一下貼文上的「+」按鈕即可加入反應。試著對此範例貼文加上反應!"
reactToContinue: "添加反應以繼續教學課程。"
reactNotification: "當有人對您的貼文做出反應時會即時接收到通知。"
reactDone: "按下「-」按鈕可以取消反應。"
@@ -1473,7 +1481,7 @@ _accountMigration:
startMigration: "遷移"
migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。"
movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。"
- postMigrationNote: "取消追蹤此帳戶將在遷移操作後 24 小時執行。\n 此帳戶有 0 個關注者/關注者。 您的關注者仍然可以看到此帳戶的關注者帖子,因為您不會被取消關注。"
+ postMigrationNote: "將在完成遷移後的 24 小時取消追隨所有帳號。\n此帳戶的追隨中/追隨者人數將歸零。由於不會解除粉絲對您的追隨,因此他們仍然可以繼續閱覽此帳戶僅對追隨者公開的貼文。"
movedTo: "要遷移到的帳戶:"
_achievements:
earnedAt: "獲得日期"
@@ -1793,7 +1801,7 @@ _role:
avatarDecorationLimit: "頭像裝飾的最大設置量"
canImportAntennas: "允許匯入天線"
canImportBlocking: "允許匯入封鎖名單"
- canImportFollowing: "允許匯入跟隨名單"
+ canImportFollowing: "允許匯入追隨名單"
canImportMuting: "允許匯入靜音名單"
canImportUserLists: "允許匯入清單"
_condition:
@@ -2069,7 +2077,7 @@ _2fa:
step4: "從現在開始,任何登入操作都將要求您提供權杖。"
securityKeyNotSupported: "您的瀏覽器不支援安全金鑰。"
registerTOTPBeforeKey: "如要註冊安全金鑰或 Passkey,請先設定驗證應用程式。"
- securityKeyInfo: "您可以設定使用支援 FIDO2 的硬體安全鎖、終端設備的指紋認證,或者 PIN 碼來登入。"
+ securityKeyInfo: "您可以設定使用支援 FIDO2 的硬體安全金鑰,以及裝置上的生物辨識、PIN 碼和密碼等來登入。"
registerSecurityKey: "註冊安全金鑰或 Passkey"
securityKeyName: "輸入金鑰名稱"
tapSecurityKey: "按照瀏覽器的說明註冊安全金鑰或 Passkey。"
@@ -2287,7 +2295,7 @@ _profile:
metadataEdit: "編輯附加資訊"
metadataDescription: "可以在個人資料中以表格形式顯示其他資訊。"
metadataLabel: "標籤"
- metadataContent: "内容"
+ metadataContent: "內容"
changeAvatar: "更換大頭貼"
changeBanner: "變更橫幅圖像"
verifiedLinkDescription: "如果輸入包含您個人資料的網站 URL,欄位旁邊將出現驗證圖示。"
@@ -2357,9 +2365,6 @@ _pages:
newPage: "建立頁面"
editPage: "編輯頁面"
readPage: "正在檢視原始碼"
- created: "頁面已建立"
- updated: "頁面已更新"
- deleted: "頁面已被刪除"
pageSetting: "頁面設定"
nameAlreadyExists: "該頁面 URL 已存在"
invalidNameTitle: "無效的頁面 URL"
@@ -2627,7 +2632,7 @@ _externalResourceInstaller:
description: "已取得資料但解析 AiScript 時發生錯誤,導致無法載入。請聯絡外掛作者。請檢查 Javascript 控制台以取得錯誤詳細資訊。"
_pluginInstallFailed:
title: "外掛安裝失敗"
- description: "安裝插件時出現問題。請再試一次。請參閱 Javascript 控制台以取得錯誤詳細資訊。"
+ description: "安裝外掛時出現問題。請再試一次。可參閱 Javascript 控制台以取得錯誤詳細資訊。"
_themeParseFailed:
title: "佈景主題解析錯誤"
description: "已取得資料但解析佈景主題時發生錯誤,導致無法載入。請聯絡佈景主題的作者。請檢查 Javascript 控制台以取得錯誤詳細資訊。"
@@ -2646,7 +2651,7 @@ _dataSaver:
description: "將不再自動載入網址預覽縮圖。"
_code:
title: "程式碼突出顯示"
- description: "如果使用了 MFM 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。"
+ description: "如果使用了程式碼突顯語法(如 MFM),則在點擊之前不會被載入。由於需要為對應的程式語言下載突顯定義檔案,因此關閉自動載入有助於減少資料流量。"
_hemisphere:
N: "北半球"
S: "南半球"
@@ -2721,6 +2726,66 @@ _contextMenu:
app: "應用程式"
appWithShift: "Shift 鍵應用程式"
native: "瀏覽器的使用者介面"
+_gridComponent:
+ _error:
+ requiredValue: "此值為必填欄位"
+ columnTypeNotSupport: "正規表達式驗證僅支援 type:text 的欄位。"
+ patternNotMatch: "此值不符合 {pattern} 中的樣式。"
+ notUnique: "此值必須是唯一的"
+_roleSelectDialog:
+ notSelected: "未選擇"
+_customEmojisManager:
+ _gridCommon:
+ copySelectionRows: "複製選取的行"
+ copySelectionRanges: "複製選取的範圍"
+ deleteSelectionRows: "刪除所選的行"
+ deleteSelectionRanges: "刪除選取範圍的行"
+ searchSettings: "搜尋設定"
+ searchSettingCaption: "詳細設定搜尋條件。"
+ searchLimit: "顯示的數量"
+ sortOrder: "排序"
+ registrationLogs: "登錄日誌"
+ registrationLogsCaption: "會顯示更新或刪除表情符號時的日誌。進行更新或刪除操作,或切換頁面、重新載入後,日誌將會消失。"
+ alertEmojisRegisterFailedDescription: "更新或刪除表情符號失敗。詳情請查看登錄日誌。"
+ _logs:
+ showSuccessLogSwitch: "顯示成功日誌"
+ failureLogNothing: "沒有失敗的日誌。"
+ logNothing: "沒有日誌。"
+ _remote:
+ selectionRowDetail: "選取行的詳細資訊"
+ importSelectionRows: "匯入選取的行"
+ importSelectionRangesRows: "匯入選取範圍的行"
+ importEmojisButton: "匯入勾選的表情符號"
+ confirmImportEmojisTitle: "匯入表情符號"
+ confirmImportEmojisDescription: "將從遠端接收的{count}個表情符號進行匯入。請務必注意表情符號的授權。是否執行此操作?"
+ _local:
+ tabTitleList: "已登錄的表情符號列表"
+ tabTitleRegister: "登錄表情符號"
+ _list:
+ emojisNothing: "沒有登錄的表情符號。"
+ markAsDeleteTargetRows: "將選取的行設為刪除對象"
+ markAsDeleteTargetRanges: "將選取範圍的行設為刪除對象\n"
+ alertUpdateEmojisNothingDescription: "沒有選取需要變更的表情符號。"
+ alertDeleteEmojisNothingDescription: "沒有選取需要刪除的表情符號。"
+ confirmMovePage: "要移動到其他頁面嗎?"
+ confirmChangeView: "要更改顯示方式嗎?"
+ confirmUpdateEmojisDescription: "將更新{count}個表情符號。是否執行此操作?"
+ confirmDeleteEmojisDescription: "將刪除勾選的{count}個表情符號。是否執行此操作?"
+ confirmResetDescription: "目前所做的所有變更都會重設。"
+ confirmMovePageDesciption: "此頁面的表情符號已被更改。 \n若未儲存就直接離開此頁面,則在此頁面進行的所有更改將會被捨棄。"
+ dialogSelectRoleTitle: "根據表情符號設定的角色進行搜尋"
+ _register:
+ uploadSettingTitle: "上傳設定"
+ uploadSettingDescription: "您可以在此畫面設定表情符號上傳時的操作。"
+ directoryToCategoryLabel: "在「類別」欄位中輸入目錄名稱"
+ directoryToCategoryCaption: "拖放目錄時,請在「類別」欄位中輸入目錄名稱。"
+ emojiInputAreaCaption: "以下列其中一種方式選擇您想要註冊的表情符號"
+ emojiInputAreaList1: "將圖片檔案或目錄拖放到此框中"
+ emojiInputAreaList2: "點擊此連結從電腦中選擇"
+ emojiInputAreaList3: "點擊此連結從雲端硬碟中選擇"
+ confirmRegisterEmojisDescription: "將列表中顯示的表情符號登錄為新的自定表情符號。是否確定?(為避免過高負荷,每次操作最多可登錄{count}個表情符號)"
+ confirmClearEmojisDescription: "放棄編輯內容並清除列表中顯示的表情符號。是否確定?"
+ confirmUploadEmojisDescription: "將拖放的{count}個檔案上傳到雲端硬碟。是否執行此操作?"
_embedCodeGen:
title: "自訂嵌入程式碼"
header: "檢視標頭 "
@@ -2744,3 +2809,34 @@ _selfXssPrevention:
_followRequest:
recieved: "收到的請求"
sent: "送出的請求"
+_remoteLookupErrors:
+ _federationNotAllowed:
+ title: "無法與這個伺服器通訊"
+ description: "與此伺服器的通訊可能被停用、或封鎖了該伺服器,或被該伺服器封鎖。\n請聯繫您的伺服器管理員。"
+ _uriInvalid:
+ title: "URI 不正確"
+ description: "輸入的 URI 有問題。請檢查是否輸入了 URI 中不能使用的字元。"
+ _requestFailed:
+ title: "請求失敗"
+ description: "與此伺服器的通訊失敗。可能是對方伺服器斷線。 此外,請檢查是否輸入了不正確或不存在的 URI。"
+ _responseInvalid:
+ title: "回應不正確"
+ description: "雖然能夠與這個伺服器通訊,但是取得的資料不正確。"
+ _responseInvalidIdHostNotMatch:
+ description: "輸入的 URI 的網域與最終取得的 URI 的網域不同。 如果您是透過第三方伺服器查詢遠端內容,請使用可在原始伺服器上取得的 URI 再次查詢。"
+ _noSuchObject:
+ title: "查無項目"
+ description: "無法找到所要求的資源,請再次檢查 URI。"
+_captcha:
+ verify: "請通過 CAPTCHA 驗證"
+ testSiteKeyMessage: "可以輸入網站金鑰和秘密金鑰的測試值來檢查預覽。\n詳細資訊請參閱以下頁面。"
+ _error:
+ _requestFailed:
+ title: "CAPTCHA 請求失敗"
+ text: "請過一段時間後再執行,或再次檢查設定。"
+ _verificationFailed:
+ title: "CAPTCHA 驗證失敗"
+ text: "請再次檢查設定是否正確。"
+ _unknown:
+ title: "CAPTCHA 錯誤"
+ text: "發生了意外的錯誤。"
diff --git a/package.json b/package.json
index 2119284b20..68ca6fd876 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sharkey",
- "version": "2024.11.2",
+ "version": "2025.2.0-dev",
"codename": "shonk",
"repository": {
"type": "git",
diff --git a/packages/backend/migration/1709126576000-optimize-emoji-index.js b/packages/backend/migration/1709126576000-optimize-emoji-index.js
new file mode 100644
index 0000000000..e4184895d0
--- /dev/null
+++ b/packages/backend/migration/1709126576000-optimize-emoji-index.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class OptimizeEmojiIndex1709126576000 {
+ name = 'OptimizeEmojiIndex1709126576000'
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE INDEX "IDX_EMOJI_ROLE_IDS" ON "emoji" using gin ("roleIdsThatCanBeUsedThisEmojiAsReaction")`)
+ await queryRunner.query(`CREATE INDEX "IDX_EMOJI_CATEGORY" ON "emoji" ("category")`)
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`DROP INDEX "IDX_EMOJI_CATEGORY"`)
+ await queryRunner.query(`DROP INDEX "IDX_EMOJI_ROLE_IDS"`)
+ }
+}
diff --git a/packages/backend/migration/1731565470048-add-activity-log.js b/packages/backend/migration/1731565470048-add-activity-log.js
new file mode 100644
index 0000000000..19c6b336af
--- /dev/null
+++ b/packages/backend/migration/1731565470048-add-activity-log.js
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AddActivityLog1731565470048 {
+ name = 'AddActivityLog1731565470048'
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE TABLE "activity_context" ("md5" text NOT NULL, "json" jsonb NOT NULL, CONSTRAINT "PK_activity_context" PRIMARY KEY ("md5"))`);
+ await queryRunner.query(`CREATE INDEX "IDK_activity_context_md5" ON "activity_context" ("md5") `);
+ await queryRunner.query(`CREATE TABLE "activity_log" ("id" character varying(32) NOT NULL, "at" TIMESTAMP WITH TIME ZONE NOT NULL, "key_id" text NOT NULL, "host" text NOT NULL, "verified" boolean NOT NULL, "accepted" boolean NOT NULL, "result" text NOT NULL, "activity" jsonb NOT NULL, "context_hash" text, "auth_user_id" character varying(32), CONSTRAINT "PK_activity_log" PRIMARY KEY ("id"))`);
+ await queryRunner.query(`CREATE INDEX "IDX_activity_log_at" ON "activity_log" ("at") `);
+ await queryRunner.query(`CREATE INDEX "IDX_activity_log_host" ON "activity_log" ("host") `);
+ await queryRunner.query(`ALTER TABLE "activity_log" ADD CONSTRAINT "FK_activity_log_context_hash" FOREIGN KEY ("context_hash") REFERENCES "activity_context"("md5") ON DELETE CASCADE ON UPDATE NO ACTION`);
+ await queryRunner.query(`ALTER TABLE "activity_log" ADD CONSTRAINT "FK_activity_log_auth_user_id" FOREIGN KEY ("auth_user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "activity_log" DROP CONSTRAINT "FK_activity_log_auth_user_id"`);
+ await queryRunner.query(`ALTER TABLE "activity_log" DROP CONSTRAINT "FK_activity_log_context_hash"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_activity_log_host"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_activity_log_at"`);
+ await queryRunner.query(`DROP TABLE "activity_log"`);
+ await queryRunner.query(`DROP INDEX "public"."IDK_activity_context_md5"`);
+ await queryRunner.query(`DROP TABLE "activity_context"`);
+ }
+}
diff --git a/packages/backend/migration/1731909785724-activity-log-timing.js b/packages/backend/migration/1731909785724-activity-log-timing.js
new file mode 100644
index 0000000000..8b72fb8972
--- /dev/null
+++ b/packages/backend/migration/1731909785724-activity-log-timing.js
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class ActivityLogTiming1731909785724 {
+ name = 'ActivityLogTiming1731909785724'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "activity_log" ADD "duration" double precision NOT NULL DEFAULT '0'`);
+ await queryRunner.query(`ALTER TABLE "activity_log" ALTER COLUMN "result" DROP NOT NULL`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`UPDATE "activity_log" SET "result" = 'not processed' WHERE "result" IS NULL`);
+ await queryRunner.query(`ALTER TABLE "activity_log" ALTER COLUMN "result" SET NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "activity_log" DROP COLUMN "duration"`);
+ }
+}
diff --git a/packages/backend/migration/1731910422761-rename-activity-log-indexes.js b/packages/backend/migration/1731910422761-rename-activity-log-indexes.js
new file mode 100644
index 0000000000..82d5a796e9
--- /dev/null
+++ b/packages/backend/migration/1731910422761-rename-activity-log-indexes.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RenameActivityLogIndexes1731910422761 {
+ name = 'RenameActivityLogIndexes1731910422761'
+
+ async up(queryRunner) {
+ await queryRunner.query(`DROP INDEX "public"."IDK_activity_context_md5"`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`CREATE INDEX "IDK_activity_context_md5" ON "activity_context" ("md5") `);
+ }
+}
diff --git a/packages/backend/migration/1731935047347-nullable-activity-log-duration.js b/packages/backend/migration/1731935047347-nullable-activity-log-duration.js
new file mode 100644
index 0000000000..2acbd2bca5
--- /dev/null
+++ b/packages/backend/migration/1731935047347-nullable-activity-log-duration.js
@@ -0,0 +1,20 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class NullableActivityLogDuration1731935047347 {
+ name = 'NullableActivityLogDuration1731935047347'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "activity_log" ALTER COLUMN "duration" DROP NOT NULL`);
+ await queryRunner.query(`ALTER TABLE "activity_log" ALTER COLUMN "duration" DROP DEFAULT`);
+ await queryRunner.query(`UPDATE "activity_log" SET "duration" = NULL WHERE "duration" = 0`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`UPDATE "activity_log" SET "duration" = 0 WHERE "duration" IS NULL`);
+ await queryRunner.query(`ALTER TABLE "activity_log" ALTER COLUMN "duration" SET DEFAULT '0'`);
+ await queryRunner.query(`ALTER TABLE "activity_log" ALTER COLUMN "duration" SET NOT NULL`);
+ }
+}
diff --git a/packages/backend/migration/1733756280460-rename_activity_log_to_ap_inbox_log.js b/packages/backend/migration/1733756280460-rename_activity_log_to_ap_inbox_log.js
new file mode 100644
index 0000000000..ad25135188
--- /dev/null
+++ b/packages/backend/migration/1733756280460-rename_activity_log_to_ap_inbox_log.js
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RenameActivityLogToApInboxLog1733756280460 {
+ name = 'RenameActivityLogToApInboxLog1733756280460'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER INDEX "IDX_activity_log_at" RENAME TO "IDX_ap_inbox_log_at"`);
+ await queryRunner.query(`ALTER INDEX "IDX_activity_log_host" RENAME TO "IDX_ap_inbox_log_host"`);
+ await queryRunner.query(`ALTER TABLE "activity_log" RENAME CONSTRAINT "PK_activity_log" TO "PK_ap_inbox_log"`);
+ await queryRunner.query(`ALTER TABLE "activity_log" RENAME CONSTRAINT "FK_activity_log_context_hash" TO "FK_ap_inbox_log_context_hash"`);
+ await queryRunner.query(`ALTER TABLE "activity_log" RENAME CONSTRAINT "FK_activity_log_auth_user_id" TO "FK_ap_inbox_log_auth_user_id"`);
+ await queryRunner.query(`ALTER TABLE "activity_log" RENAME TO "ap_inbox_log"`);
+
+ await queryRunner.query(`ALTER TABLE "activity_context" RENAME CONSTRAINT "PK_activity_context" TO "PK_ap_context"`);
+ await queryRunner.query(`ALTER TABLE "activity_context" RENAME TO "ap_context"`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "ap_context" RENAME TO "activity_context"`);
+ await queryRunner.query(`ALTER TABLE "activity_context" RENAME CONSTRAINT "PK_ap_context" TO "PK_activity_context"`);
+
+ await queryRunner.query(`ALTER TABLE "ap_inbox_log" RENAME TO "activity_log"`);
+ await queryRunner.query(`ALTER TABLE "activity_log" RENAME CONSTRAINT "FK_ap_inbox_log_auth_user_id" TO "FK_activity_log_auth_user_id"`);
+ await queryRunner.query(`ALTER TABLE "activity_log" RENAME CONSTRAINT "FK_ap_inbox_log_context_hash" TO "FK_activity_log_context_hash"`);
+ await queryRunner.query(`ALTER TABLE "activity_log" RENAME CONSTRAINT "PK_ap_inbox_log" TO "PK_activity_log"`);
+ await queryRunner.query(`ALTER INDEX "IDX_ap_inbox_log_host" RENAME TO "IDX_activity_log_host"`);
+ await queryRunner.query(`ALTER INDEX "IDX_ap_inbox_log_at" RENAME TO "IDX_activity_log_at"`);
+ }
+}
diff --git a/packages/backend/migration/1738043621143-add_user_mandatoryCW.js b/packages/backend/migration/1738043621143-add_user_mandatoryCW.js
new file mode 100644
index 0000000000..dd05076dd2
--- /dev/null
+++ b/packages/backend/migration/1738043621143-add_user_mandatoryCW.js
@@ -0,0 +1,11 @@
+export class AddUserMandatoryCW1738043621143 {
+ name = 'AddUserCW1738043621143'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user" ADD "mandatoryCW" text`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "mandatoryCW"`);
+ }
+}
diff --git a/packages/backend/migration/1738293576355-create_ap_fetch_log.js b/packages/backend/migration/1738293576355-create_ap_fetch_log.js
new file mode 100644
index 0000000000..4371f50b4a
--- /dev/null
+++ b/packages/backend/migration/1738293576355-create_ap_fetch_log.js
@@ -0,0 +1,19 @@
+export class CreateApFetchLog1738293576355 {
+ name = 'CreateApFetchLog1738293576355'
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE TABLE "ap_fetch_log" ("id" character varying(32) NOT NULL, "at" TIMESTAMP WITH TIME ZONE NOT NULL, "duration" double precision, "host" text NOT NULL, "request_uri" text NOT NULL, "object_uri" text, "accepted" boolean, "result" text, "object" jsonb, "context_hash" text, CONSTRAINT "PK_ap_fetch_log" PRIMARY KEY ("id"))`);
+ await queryRunner.query(`CREATE INDEX "IDX_ap_fetch_log_at" ON "ap_fetch_log" ("at") `);
+ await queryRunner.query(`CREATE INDEX "IDX_ap_fetch_log_host" ON "ap_fetch_log" ("host") `);
+ await queryRunner.query(`CREATE INDEX "IDX_ap_fetch_log_object_uri" ON "ap_fetch_log" ("object_uri") `);
+ await queryRunner.query(`ALTER TABLE "ap_fetch_log" ADD CONSTRAINT "FK_ap_fetch_log_context_hash" FOREIGN KEY ("context_hash") REFERENCES "ap_context"("md5") ON DELETE CASCADE ON UPDATE NO ACTION`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "ap_fetch_log" DROP CONSTRAINT "FK_ap_fetch_log_context_hash"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ap_fetch_log_object_uri"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ap_fetch_log_host"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ap_fetch_log_at"`);
+ await queryRunner.query(`DROP TABLE "ap_fetch_log"`);
+ }
+}
diff --git a/packages/backend/migration/1738346484187-robotsTxt.js b/packages/backend/migration/1738346484187-robotsTxt.js
new file mode 100644
index 0000000000..00ea1fb030
--- /dev/null
+++ b/packages/backend/migration/1738346484187-robotsTxt.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RobotsTxt1738346484187 {
+ name = 'RobotsTxt1738346484187'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "robotsTxt" text`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "robotsTxt"`);
+ }
+}
diff --git a/packages/backend/migration/1738446745738-add_user_profile_default_cw.js b/packages/backend/migration/1738446745738-add_user_profile_default_cw.js
new file mode 100644
index 0000000000..205ca2087a
--- /dev/null
+++ b/packages/backend/migration/1738446745738-add_user_profile_default_cw.js
@@ -0,0 +1,11 @@
+export class AddUserProfileDefaultCw1738446745738 {
+ name = 'AddUserProfileDefaultCw1738446745738'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" ADD "default_cw" text`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "default_cw"`);
+ }
+}
diff --git a/packages/backend/migration/1738468079662-add_user_profile_default_cw_priority.js b/packages/backend/migration/1738468079662-add_user_profile_default_cw_priority.js
new file mode 100644
index 0000000000..90de25e06f
--- /dev/null
+++ b/packages/backend/migration/1738468079662-add_user_profile_default_cw_priority.js
@@ -0,0 +1,13 @@
+export class AddUserProfileDefaultCwPriority1738468079662 {
+ name = 'AddUserProfileDefaultCwPriority1738468079662'
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE TYPE "public"."user_profile_default_cw_priority_enum" AS ENUM ('default', 'parent', 'defaultParent', 'parentDefault')`);
+ await queryRunner.query(`ALTER TABLE "user_profile" ADD "default_cw_priority" "public"."user_profile_default_cw_priority_enum" NOT NULL DEFAULT 'parent'`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "default_cw_priority"`);
+ await queryRunner.query(`DROP TYPE "public"."user_profile_default_cw_priority_enum"`);
+ }
+}
diff --git a/packages/backend/migration/1739451520729-index_note_attachedFileTypes.js b/packages/backend/migration/1739451520729-index_note_attachedFileTypes.js
new file mode 100644
index 0000000000..351908a68c
--- /dev/null
+++ b/packages/backend/migration/1739451520729-index_note_attachedFileTypes.js
@@ -0,0 +1,12 @@
+// https://stackoverflow.com/a/4059785
+export class IndexNoteAttachedFileTypes1739451520729 {
+ name = 'IndexNoteAttachedFileTypes1739451520729'
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE INDEX "IDX_NOTE_ATTACHED_FILE_TYPES" ON "note" USING GIN ("attachedFileTypes" array_ops)`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`DROP INDEX "IDX_NOTE_ATTACHED_FILE_TYPES"`);
+ }
+}
diff --git a/packages/backend/migration/1739671352784-add_note_processErrors.js b/packages/backend/migration/1739671352784-add_note_processErrors.js
new file mode 100644
index 0000000000..0be10125e1
--- /dev/null
+++ b/packages/backend/migration/1739671352784-add_note_processErrors.js
@@ -0,0 +1,11 @@
+export class AddNoteProcessErrors1739671352784 {
+ name = 'AddNoteProcessErrors1739671352784'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "note" ADD "processErrors" text array`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "processErrors"`);
+ }
+}
diff --git a/packages/backend/migration/1739671777344-add_user_rejectQuotes.js b/packages/backend/migration/1739671777344-add_user_rejectQuotes.js
new file mode 100644
index 0000000000..29ed90c8ff
--- /dev/null
+++ b/packages/backend/migration/1739671777344-add_user_rejectQuotes.js
@@ -0,0 +1,11 @@
+export class AddUserRejectQuotes1739671777344 {
+ name = 'AddUserRejectQuotes1739671777344'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user" ADD "rejectQuotes" boolean NOT NULL DEFAULT false`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "rejectQuotes"`);
+ }
+}
diff --git a/packages/backend/migration/1739671847942-add_instance_rejectQuotes.js b/packages/backend/migration/1739671847942-add_instance_rejectQuotes.js
new file mode 100644
index 0000000000..89774eb991
--- /dev/null
+++ b/packages/backend/migration/1739671847942-add_instance_rejectQuotes.js
@@ -0,0 +1,11 @@
+export class AddInstanceRejectQuotes1739671847942 {
+ name = 'AddInstanceRejectQuotes1739671847942'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "instance" ADD "rejectQuotes" boolean NOT NULL DEFAULT false`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "rejectQuotes"`);
+ }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index dd6c9cc792..277aed4f79 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
- "node": "^20.10.0 || ^22.0.0"
+ "node": "^22.0.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",
@@ -81,7 +81,6 @@
"@fastify/view": "10.0.1",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
- "@napi-rs/canvas": "0.1.56",
"@nestjs/common": "10.4.7",
"@nestjs/core": "10.4.7",
"@nestjs/testing": "10.4.7",
@@ -106,6 +105,7 @@
"body-parser": "1.20.3",
"bullmq": "5.26.1",
"cacheable-lookup": "7.0.0",
+ "canvas": "^3.1.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
@@ -164,7 +164,6 @@
"proxy-addr": "^2.0.7",
"psl": "^1.13.0",
"pug": "3.0.3",
- "punycode": "2.3.1",
"qrcode": "1.5.4",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
@@ -223,7 +222,6 @@
"@types/pg": "8.11.10",
"@types/proxy-addr": "^2.0.3",
"@types/pug": "2.0.10",
- "@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js
index 17b198ef62..b15ce51ec8 100644
--- a/packages/backend/scripts/check_connect.js
+++ b/packages/backend/scripts/check_connect.js
@@ -51,6 +51,7 @@ const promises = Array
config.redisForJobQueue,
config.redisForTimelines,
config.redisForReactions,
+ config.redisForRateLimit,
]))
.map(connectToRedis)
.concat([
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 6ae8ccfbb3..7ca566477d 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -7,14 +7,14 @@ import { Global, Inject, Module } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
import { MeiliSearch } from 'meilisearch';
+import { MiMeta } from '@/models/Meta.js';
import { DI } from './di-symbols.js';
import { Config, loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import { allSettled } from './misc/promise-tracker.js';
-import type { Provider, OnApplicationShutdown } from '@nestjs/common';
-import { MiMeta } from '@/models/Meta.js';
import { GlobalEvents } from './core/GlobalEventService.js';
+import type { Provider, OnApplicationShutdown } from '@nestjs/common';
const $config: Provider = {
provide: DI.config,
@@ -33,7 +33,11 @@ const $db: Provider = {
const $meilisearch: Provider = {
provide: DI.meilisearch,
useFactory: (config: Config) => {
- if (config.meilisearch) {
+ if (config.fulltextSearch?.provider === 'meilisearch') {
+ if (!config.meilisearch) {
+ throw new Error('MeiliSearch is enabled but no configuration is provided');
+ }
+
return new MeiliSearch({
host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
apiKey: config.meilisearch.apiKey,
@@ -88,6 +92,14 @@ const $redisForReactions: Provider = {
inject: [DI.config],
};
+const $redisForRateLimit: Provider = {
+ provide: DI.redisForRateLimit,
+ useFactory: (config: Config) => {
+ return new Redis.Redis(config.redisForRateLimit);
+ },
+ inject: [DI.config],
+};
+
const $meta: Provider = {
provide: DI.meta,
useFactory: async (db: DataSource, redisForSub: Redis.Redis) => {
@@ -148,8 +160,8 @@ const $meta: Provider = {
@Global()
@Module({
imports: [RepositoryModule],
- providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions],
- exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule],
+ providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, $redisForRateLimit],
+ exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, $redisForRateLimit, RepositoryModule],
})
export class GlobalModule implements OnApplicationShutdown {
constructor(
@@ -159,6 +171,7 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
@Inject(DI.redisForReactions) private redisForReactions: Redis.Redis,
+ @Inject(DI.redisForRateLimit) private redisForRateLimit: Redis.Redis,
) { }
public async dispose(): Promise<void> {
@@ -172,6 +185,7 @@ export class GlobalModule implements OnApplicationShutdown {
this.redisForSub.disconnect(),
this.redisForTimelines.disconnect(),
this.redisForReactions.disconnect(),
+ this.redisForRateLimit.disconnect(),
]);
}
diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts
index ad59a55688..2f97980e9a 100644
--- a/packages/backend/src/boot/common.ts
+++ b/packages/backend/src/boot/common.ts
@@ -13,6 +13,7 @@ import { ServerStatsService } from '@/daemons/ServerStatsService.js';
import { ServerService } from '@/server/ServerService.js';
import { MainModule } from '@/MainModule.js';
import { envOption } from '@/env.js';
+import { ApLogCleanupService } from '@/daemons/ApLogCleanupService.js';
export async function server() {
const app = await NestFactory.createApplicationContext(MainModule, {
@@ -28,6 +29,7 @@ export async function server() {
if (!envOption.noDaemons) {
app.get(QueueStatsService).start();
app.get(ServerStatsService).start();
+ app.get(ApLogCleanupService).start();
}
return app;
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index 56128a7ab9..735a0f4666 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -70,14 +70,22 @@ async function main() {
});
//#endregion
- if (cluster.isPrimary || envOption.disableClustering) {
- await masterMain();
+ if (!envOption.disableClustering) {
if (cluster.isPrimary) {
+ logger.info(`Start main process... pid: ${process.pid}`);
+ await masterMain();
ev.mount();
+ } else if (cluster.isWorker) {
+ logger.info(`Start worker process... pid: ${process.pid}`);
+ await workerMain();
+ } else {
+ throw new Error('Unknown process type');
}
- }
- if (cluster.isWorker) {
- await workerMain();
+ } else {
+ // 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない)
+ logger.info(`Start main process... pid: ${process.pid}`);
+ await masterMain();
+ ev.mount();
}
readyRef.value = true;
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 355e095c12..cf9e9a9bae 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -18,6 +18,7 @@ import type { Config } from '@/config.js';
import { showMachineInfo } from '@/misc/show-machine-info.js';
import { envOption } from '@/env.js';
import { jobQueue, server } from './common.js';
+import * as net from 'node:net';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@@ -32,7 +33,6 @@ const themeColor = chalk.hex('#86b300');
function greet() {
if (!envOption.quiet) {
//#region Misskey logo
- const v = `v${meta.version}`;
console.log(themeColor(' _____ _ _ '));
console.log(themeColor('/ ___| | | | '));
console.log(themeColor('\\ `--.| |__ __ _ _ __| | _____ _ _ '));
@@ -91,42 +91,59 @@ export async function masterMain() {
maxBreadcrumbs: 0,
// Set release version
- release: "Sharkey@" + meta.version,
+ release: 'Sharkey@' + meta.version,
...config.sentryForBackend.options,
});
}
- if (envOption.disableClustering) {
+ bootLogger.info(
+ `mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`,
+ );
+
+ if (envOption.onlyServer && envOption.onlyQueue) {
+ bootLogger.error('Configuration error: onlyServer and onlyQueue cannot both be set. To run both server and queue workers, disable / remove both options.');
+ process.exit(1);
+ }
+
+ if (!envOption.disableClustering) {
+ // clusterモジュール有効時
+
if (envOption.onlyServer) {
- await server();
+ // onlyServer かつ enableCluster な場合、メインプロセスはforkのみに制限する(listenしない)。
+ // ワーカープロセス側でlistenすると、メインプロセスでポートへの着信を受け入れてワーカープロセスへの分配を行う動作をする。
+ // そのため、メインプロセスでも直接listenするとポートの競合が発生して起動に失敗してしまう。
+ // see: https://nodejs.org/api/cluster.html#cluster
} else if (envOption.onlyQueue) {
await jobQueue();
} else {
await server();
- await jobQueue();
- }
- } else {
- if (envOption.onlyServer) {
- // nop
- } else if (envOption.onlyQueue) {
- // nop
- } else {
- await server();
}
if (config.clusterLimit === 0) {
- bootLogger.error("Configuration error: we can't create workers, `config.clusterLimit` is 0 (if you don't want to use clustering, set the environment variable `MK_DISABLE_CLUSTERING` to a non-empty value instead)", null, true);
+ bootLogger.error('Configuration error: we can\'t create workers, `config.clusterLimit` is 0 (if you don\'t want to use clustering, set the environment variable `MK_DISABLE_CLUSTERING` to a non-empty value instead)', null, true);
process.exit(1);
}
await spawnWorkers(config.clusterLimit);
+ } else {
+ // clusterモジュール無効時
+
+ if (envOption.onlyServer) {
+ await server();
+ } else if (envOption.onlyQueue) {
+ await jobQueue();
+ } else {
+ await server();
+ await jobQueue();
+ }
}
if (envOption.onlyQueue) {
bootLogger.succ('Queue started', null, true);
} else {
- bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on port ${config.port} on ${config.url}`, null, true);
+ const addressString = net.isIPv6(config.address) ? `[${config.address}]` : config.address;
+ bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on ${addressString}:${config.port} on ${config.url}`, null, true);
}
}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 4af1140f36..c571c227a1 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -27,6 +27,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
type Source = {
url?: string;
port?: number;
+ address?: string;
socket?: string;
chmodSocket?: string;
disableHsts?: boolean;
@@ -52,6 +53,10 @@ type Source = {
redisForJobQueue?: RedisOptionsSource;
redisForTimelines?: RedisOptionsSource;
redisForReactions?: RedisOptionsSource;
+ redisForRateLimit?: RedisOptionsSource;
+ fulltextSearch?: {
+ provider?: FulltextSearchProvider;
+ };
meilisearch?: {
host: string;
port: string;
@@ -118,11 +123,25 @@ type Source = {
pidFile: string;
filePermissionBits?: string;
+
+ logging?: {
+ sql?: {
+ disableQueryTruncation? : boolean,
+ enableQueryParamLogging? : boolean,
+ }
+ }
+
+ activityLogging?: {
+ enabled?: boolean;
+ preSave?: boolean;
+ maxAge?: number;
+ };
};
export type Config = {
url: string;
port: number;
+ address: string;
socket: string | undefined;
chmodSocket: string | undefined;
disableHsts: boolean | undefined;
@@ -143,6 +162,9 @@ export type Config = {
user: string;
pass: string;
}[] | undefined;
+ fulltextSearch?: {
+ provider?: FulltextSearchProvider;
+ };
meilisearch: {
host: string;
port: string;
@@ -179,6 +201,12 @@ export type Config = {
signToActivityPubGet: boolean;
attachLdSignatureForRelays: boolean;
checkActivityPubGetSignature: boolean | undefined;
+ logging?: {
+ sql?: {
+ disableQueryTruncation? : boolean,
+ enableQueryParamLogging? : boolean,
+ }
+ }
version: string;
publishTarballInsteadOfProvideRepositoryUrl: boolean;
@@ -204,6 +232,7 @@ export type Config = {
redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
redisForReactions: RedisOptions & RedisOptionsSource;
+ redisForRateLimit: RedisOptions & RedisOptionsSource;
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
perChannelMaxNoteCacheCount: number;
@@ -217,8 +246,16 @@ export type Config = {
pidFile: string;
filePermissionBits?: string;
+
+ activityLogging: {
+ enabled: boolean;
+ preSave: boolean;
+ maxAge: number;
+ };
};
+export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch' | 'sqlTsvector';
+
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@@ -288,6 +325,7 @@ export function loadConfig(): Config {
setupPassword: config.setupPassword,
url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '3000', 10),
+ address: config.address ?? '0.0.0.0',
socket: config.socket,
chmodSocket: config.chmodSocket,
disableHsts: config.disableHsts,
@@ -302,12 +340,14 @@ export function loadConfig(): Config {
db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves,
+ fulltextSearch: config.fulltextSearch,
meilisearch: config.meilisearch,
redis,
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
+ redisForRateLimit: config.redisForRateLimit ? convertRedisOptions(config.redisForRateLimit, host) : redis,
sentryForBackend: config.sentryForBackend,
sentryForFrontend: config.sentryForFrontend,
id: config.id,
@@ -354,6 +394,12 @@ export function loadConfig(): Config {
import: config.import,
pidFile: config.pidFile,
filePermissionBits: config.filePermissionBits,
+ logging: config.logging,
+ activityLogging: {
+ enabled: config.activityLogging?.enabled ?? false,
+ preSave: config.activityLogging?.preSave ?? false,
+ maxAge: config.activityLogging?.maxAge ?? (1000 * 60 * 60 * 24 * 30),
+ },
};
}
@@ -488,13 +534,14 @@ function applyEnvOverrides(config: Source) {
// these are all the settings that can be overridden
- _apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts', 'id', 'dbReplications']]);
+ _apply_top([['url', 'port', 'address', 'socket', 'chmodSocket', 'disableHsts', 'id', 'dbReplications']]);
_apply_top(['db', ['host', 'port', 'db', 'user', 'pass', 'disableCache']]);
_apply_top(['dbSlaves', Array.from((config.dbSlaves ?? []).keys()), ['host', 'port', 'db', 'user', 'pass']]);
_apply_top([
- ['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines', 'redisForReactions'],
+ ['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines', 'redisForReactions', 'redisForRateLimit'],
['host', 'port', 'username', 'pass', 'db', 'prefix'],
]);
+ _apply_top(['fulltextSearch', 'provider']);
_apply_top(['meilisearch', ['host', 'port', 'apikey', 'ssl', 'index', 'scope']]);
_apply_top([['sentryForFrontend', 'sentryForBackend'], 'options', ['dsn', 'profileSampleRate', 'serverName', 'includeLocalVariables', 'proxy', 'keepAlive', 'caCerts']]);
_apply_top(['sentryForBackend', 'enableNodeProfiling']);
@@ -503,4 +550,6 @@ function applyEnvOverrides(config: Source) {
_apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile', 'filePermissionBits']]);
_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature', 'setupPassword']]);
+ _apply_top(['logging', 'sql', ['disableQueryTruncation', 'enableQueryParamLogging']]);
+ _apply_top(['activityLogging', ['enabled', 'preSave', 'maxAge']]);
}
diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts
index adb0a63ad7..e2c492ff80 100644
--- a/packages/backend/src/const.ts
+++ b/packages/backend/src/const.ts
@@ -8,6 +8,18 @@ export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
+export const FILE_TYPE_IMAGE = [
+ 'image/png',
+ 'image/gif',
+ 'image/jpeg',
+ 'image/webp',
+ 'image/avif',
+ 'image/apng',
+ 'image/bmp',
+ 'image/tiff',
+ 'image/x-icon',
+];
+
// ブラウザで直接表示することを許可するファイルの種類のリスト
// ここに含まれないものは application/octet-stream としてレスポンスされる
// SVGはXSSを生むので許可しない
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index 742e2621fd..9bca795479 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -160,22 +160,22 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
};
});
- const recipientWebhookIds = await this.fetchWebhookRecipients()
- .then(it => it
- .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
- .map(it => it.systemWebhookId)
- .filter(x => x != null));
- for (const webhookId of recipientWebhookIds) {
- await Promise.all(
- convertedReports.map(it => {
- return this.systemWebhookService.enqueueSystemWebhook(
- webhookId,
- type,
- it,
- );
- }),
- );
- }
+ const inactiveRecipients = await this.fetchWebhookRecipients()
+ .then(it => it.filter(it => !it.isActive));
+ const withoutWebhookIds = inactiveRecipients
+ .map(it => it.systemWebhookId)
+ .filter(x => x != null);
+ return Promise.all(
+ convertedReports.map(it => {
+ return this.systemWebhookService.enqueueSystemWebhook(
+ type,
+ it,
+ {
+ excludes: withoutWebhookIds,
+ },
+ );
+ }),
+ );
}
/**
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 24d11f29ff..e24fefb4b5 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
-import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
+import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository, NoteScheduleRepository, MiNoteSchedule } from '@/models/_.js';
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
import { IdService } from '@/core/IdService.js';
@@ -49,6 +49,9 @@ export class AccountMoveService {
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
+ @Inject(DI.noteScheduleRepository)
+ private noteScheduleRepository: NoteScheduleRepository,
+
private userEntityService: UserEntityService,
private idService: IdService,
private apPersonService: ApPersonService,
@@ -119,6 +122,7 @@ export class AccountMoveService {
await Promise.all([
this.copyBlocking(src, dst),
this.copyMutings(src, dst),
+ this.deleteScheduledNotes(src),
this.updateLists(src, dst),
]);
} catch {
@@ -201,6 +205,21 @@ export class AccountMoveService {
await this.mutingsRepository.insert(arrayToInsert);
}
+ @bindThis
+ public async deleteScheduledNotes(src: ThinUser): Promise<void> {
+ const scheduledNotes = await this.noteScheduleRepository.findBy({
+ userId: src.id,
+ }) as MiNoteSchedule[];
+
+ for (const note of scheduledNotes) {
+ await this.queueService.ScheduleNotePostQueue.remove(`schedNote:${note.id}`);
+ }
+
+ await this.noteScheduleRepository.delete({
+ userId: src.id,
+ });
+ }
+
/**
* Update lists while moving accounts.
* - No removal of the old account from the lists
diff --git a/packages/backend/src/core/ApLogService.ts b/packages/backend/src/core/ApLogService.ts
new file mode 100644
index 0000000000..096ec21de7
--- /dev/null
+++ b/packages/backend/src/core/ApLogService.ts
@@ -0,0 +1,207 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { createHash } from 'crypto';
+import { Inject, Injectable } from '@nestjs/common';
+import { In, LessThan } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { SkApFetchLog, SkApInboxLog, SkApContext } from '@/models/_.js';
+import type { ApContextsRepository, ApFetchLogsRepository, ApInboxLogsRepository } from '@/models/_.js';
+import type { Config } from '@/config.js';
+import { JsonValue } from '@/misc/json-value.js';
+import { UtilityService } from '@/core/UtilityService.js';
+import { IdService } from '@/core/IdService.js';
+import { IActivity, IObject } from './activitypub/type.js';
+
+@Injectable()
+export class ApLogService {
+ constructor(
+ @Inject(DI.config)
+ private readonly config: Config,
+
+ @Inject(DI.apContextsRepository)
+ private apContextsRepository: ApContextsRepository,
+
+ @Inject(DI.apInboxLogsRepository)
+ private readonly apInboxLogsRepository: ApInboxLogsRepository,
+
+ @Inject(DI.apFetchLogsRepository)
+ private readonly apFetchLogsRepository: ApFetchLogsRepository,
+
+ private readonly utilityService: UtilityService,
+ private readonly idService: IdService,
+ ) {}
+
+ /**
+ * Creates an inbox log from an activity, and saves it if pre-save is enabled.
+ */
+ public async createInboxLog(data: Partial<SkApInboxLog> & {
+ activity: IActivity,
+ keyId: string,
+ }): Promise<SkApInboxLog> {
+ const { object: activity, context, contextHash } = extractObjectContext(data.activity);
+ const host = this.utilityService.extractDbHost(data.keyId);
+
+ const log = new SkApInboxLog({
+ id: this.idService.gen(),
+ at: new Date(),
+ verified: false,
+ accepted: false,
+ host,
+ ...data,
+ activity,
+ context,
+ contextHash,
+ });
+
+ if (this.config.activityLogging.preSave) {
+ await this.saveInboxLog(log);
+ }
+
+ return log;
+ }
+
+ /**
+ * Saves or finalizes an inbox log.
+ */
+ public async saveInboxLog(log: SkApInboxLog): Promise<SkApInboxLog> {
+ if (log.context) {
+ await this.saveContext(log.context);
+ }
+
+ // Will be UPDATE with preSave, and INSERT without.
+ await this.apInboxLogsRepository.upsert(log, ['id']);
+ return log;
+ }
+
+ /**
+ * Creates a fetch log from an activity, and saves it if pre-save is enabled.
+ */
+ public async createFetchLog(data: Partial<SkApFetchLog> & {
+ requestUri: string
+ host: string,
+ }): Promise<SkApFetchLog> {
+ const log = new SkApFetchLog({
+ id: this.idService.gen(),
+ at: new Date(),
+ accepted: false,
+ ...data,
+ });
+
+ if (this.config.activityLogging.preSave) {
+ await this.saveFetchLog(log);
+ }
+
+ return log;
+ }
+
+ /**
+ * Saves or finalizes a fetch log.
+ */
+ public async saveFetchLog(log: SkApFetchLog): Promise<SkApFetchLog> {
+ if (log.context) {
+ await this.saveContext(log.context);
+ }
+
+ // Will be UPDATE with preSave, and INSERT without.
+ await this.apFetchLogsRepository.upsert(log, ['id']);
+ return log;
+ }
+
+ private async saveContext(context: SkApContext): Promise<void> {
+ // https://stackoverflow.com/a/47064558
+ await this.apContextsRepository
+ .createQueryBuilder('activity_context')
+ .insert()
+ .into(SkApContext)
+ .values(context)
+ .orIgnore('md5')
+ .execute();
+ }
+
+ /**
+ * Deletes all logged copies of an object or objects
+ * @param objectUris URIs / AP IDs of the objects to delete
+ */
+ public async deleteObjectLogs(objectUris: string | string[]): Promise<number> {
+ if (Array.isArray(objectUris)) {
+ const logsDeleted = await this.apFetchLogsRepository.delete({
+ objectUri: In(objectUris),
+ });
+ return logsDeleted.affected ?? 0;
+ } else {
+ const logsDeleted = await this.apFetchLogsRepository.delete({
+ objectUri: objectUris,
+ });
+ return logsDeleted.affected ?? 0;
+ }
+ }
+
+ /**
+ * Deletes all expired AP logs and garbage-collects the AP context cache.
+ * Returns the total number of deleted rows.
+ */
+ public async deleteExpiredLogs(): Promise<number> {
+ // This is the date in UTC of the oldest log to KEEP
+ const oldestAllowed = new Date(Date.now() - this.config.activityLogging.maxAge);
+
+ // Delete all logs older than the threshold.
+ const inboxDeleted = await this.deleteExpiredInboxLogs(oldestAllowed);
+ const fetchDeleted = await this.deleteExpiredFetchLogs(oldestAllowed);
+
+ return inboxDeleted + fetchDeleted;
+ }
+
+ private async deleteExpiredInboxLogs(oldestAllowed: Date): Promise<number> {
+ const { affected } = await this.apInboxLogsRepository.delete({
+ at: LessThan(oldestAllowed),
+ });
+
+ return affected ?? 0;
+ }
+
+ private async deleteExpiredFetchLogs(oldestAllowed: Date): Promise<number> {
+ const { affected } = await this.apFetchLogsRepository.delete({
+ at: LessThan(oldestAllowed),
+ });
+
+ return affected ?? 0;
+ }
+}
+
+export function extractObjectContext<T extends IObject>(input: T) {
+ const object = Object.assign({}, input, { '@context': undefined }) as Omit<T, '@context'>;
+ const { context, contextHash } = parseContext(input['@context']);
+
+ return { object, context, contextHash };
+}
+
+export function parseContext(input: JsonValue | undefined): { contextHash: string | null, context: SkApContext | null } {
+ // Empty contexts are excluded for easier querying
+ if (input == null) {
+ return {
+ contextHash: null,
+ context: null,
+ };
+ }
+
+ const contextHash = createHash('md5').update(JSON.stringify(input)).digest('base64');
+ const context = new SkApContext({
+ md5: contextHash,
+ json: input,
+ });
+ return { contextHash, context };
+}
+
+export function calculateDurationSince(startTime: bigint): number {
+ // Calculate the processing time with correct rounding and decimals.
+ // 1. Truncate nanoseconds to microseconds
+ // 2. Scale to 1/10 millisecond ticks.
+ // 3. Round to nearest tick.
+ // 4. Sale to milliseconds
+ // Example: 123,456,789 ns -> 123,456 us -> 12,345.6 ticks -> 12,346 ticks -> 123.46 ms
+ const endTime = process.hrtime.bigint();
+ return Math.round(Number((endTime - startTime) / 1000n) / 10) / 100;
+}
diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index 5b1ab00cfe..d17101ac97 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -6,6 +6,69 @@
import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
+import { MetaService } from '@/core/MetaService.js';
+import { MiMeta } from '@/models/Meta.js';
+import Logger from '@/logger.js';
+import { LoggerService } from './LoggerService.js';
+
+export const supportedCaptchaProviders = ['none', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'fc', 'testcaptcha'] as const;
+export type CaptchaProvider = typeof supportedCaptchaProviders[number];
+
+export const captchaErrorCodes = {
+ invalidProvider: Symbol('invalidProvider'),
+ invalidParameters: Symbol('invalidParameters'),
+ noResponseProvided: Symbol('noResponseProvided'),
+ requestFailed: Symbol('requestFailed'),
+ verificationFailed: Symbol('verificationFailed'),
+ unknown: Symbol('unknown'),
+} as const;
+export type CaptchaErrorCode = typeof captchaErrorCodes[keyof typeof captchaErrorCodes];
+
+export type CaptchaSetting = {
+ provider: CaptchaProvider;
+ hcaptcha: {
+ siteKey: string | null;
+ secretKey: string | null;
+ }
+ mcaptcha: {
+ siteKey: string | null;
+ secretKey: string | null;
+ instanceUrl: string | null;
+ }
+ recaptcha: {
+ siteKey: string | null;
+ secretKey: string | null;
+ }
+ turnstile: {
+ siteKey: string | null;
+ secretKey: string | null;
+ }
+ fc: {
+ siteKey: string | null;
+ secretKey: string | null;
+ }
+}
+
+export class CaptchaError extends Error {
+ public readonly code: CaptchaErrorCode;
+ public readonly cause?: unknown;
+
+ constructor(code: CaptchaErrorCode, message: string, cause?: unknown) {
+ super(message);
+ this.code = code;
+ this.cause = cause;
+ this.name = 'CaptchaError';
+ }
+}
+
+export type CaptchaSaveSuccess = {
+ success: true;
+}
+export type CaptchaSaveFailure = {
+ success: false;
+ error: CaptchaError;
+}
+export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure;
type CaptchaResponse = {
success: boolean;
@@ -15,9 +78,14 @@ type CaptchaResponse = {
@Injectable()
export class CaptchaService {
+ private readonly logger: Logger;
+
constructor(
private httpRequestService: HttpRequestService,
+ private metaService: MetaService,
+ loggerService: LoggerService,
) {
+ this.logger = loggerService.getLogger('captcha');
}
@bindThis
@@ -45,39 +113,39 @@ export class CaptchaService {
@bindThis
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {
- throw new Error('recaptcha-failed: no response provided');
+ throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'recaptcha-failed: no response provided');
}
const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => {
- throw new Error(`recaptcha-request-failed: ${err}`);
+ throw new CaptchaError(captchaErrorCodes.requestFailed, `recaptcha-request-failed: ${err}`);
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
- throw new Error(`recaptcha-failed: ${errorCodes}`);
+ throw new CaptchaError(captchaErrorCodes.verificationFailed, `recaptcha-failed: ${errorCodes}`);
}
}
@bindThis
public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {
- throw new Error('hcaptcha-failed: no response provided');
+ throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'hcaptcha-failed: no response provided');
}
const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
- throw new Error(`hcaptcha-request-failed: ${err}`);
+ throw new CaptchaError(captchaErrorCodes.requestFailed, `hcaptcha-request-failed: ${err}`);
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
- throw new Error(`hcaptcha-failed: ${errorCodes}`);
+ throw new CaptchaError(captchaErrorCodes.verificationFailed, `hcaptcha-failed: ${errorCodes}`);
}
}
@bindThis
public async verifyFriendlyCaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {
- throw new Error('frc-failed: no response provided');
+ throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'frc-failed: no response provided');
}
const result = await this.httpRequestService.send('https://api.friendlycaptcha.com/api/v1/siteverify', {
@@ -89,17 +157,17 @@ export class CaptchaService {
headers: {
'Content-Type': 'application/json',
},
- });
+ }, { throwErrorWhenResponseNotOk: false });
if (result.status !== 200) {
- throw new Error('frc-failed: frc didn\'t return 200 OK');
+ throw new CaptchaError(captchaErrorCodes.requestFailed, `frc-request-failed: ${result.status}`);
}
const resp = await result.json() as CaptchaResponse;
if (resp.success !== true) {
const errorCodes = resp['errors'] ? resp['errors'].join(', ') : '';
- throw new Error(`frc-failed: ${errorCodes}`);
+ throw new CaptchaError(captchaErrorCodes.verificationFailed, `frc-failed: ${errorCodes}`);
}
}
@@ -107,7 +175,7 @@ export class CaptchaService {
@bindThis
public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> {
if (response == null) {
- throw new Error('mcaptcha-failed: no response provided');
+ throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'mcaptcha-failed: no response provided');
}
const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost);
@@ -121,46 +189,272 @@ export class CaptchaService {
headers: {
'Content-Type': 'application/json',
},
- });
+ }, { throwErrorWhenResponseNotOk: false });
if (result.status !== 200) {
- throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK');
+ throw new CaptchaError(captchaErrorCodes.requestFailed, 'mcaptcha-failed: mcaptcha didn\'t return 200 OK');
}
const resp = (await result.json()) as { valid: boolean };
if (!resp.valid) {
- throw new Error('mcaptcha-request-failed');
+ throw new CaptchaError(captchaErrorCodes.verificationFailed, 'mcaptcha-request-failed');
}
}
@bindThis
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) {
- throw new Error('turnstile-failed: no response provided');
+ throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'turnstile-failed: no response provided');
}
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
- throw new Error(`turnstile-request-failed: ${err}`);
+ throw new CaptchaError(captchaErrorCodes.requestFailed, `turnstile-request-failed: ${err}`);
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
- throw new Error(`turnstile-failed: ${errorCodes}`);
+ throw new CaptchaError(captchaErrorCodes.verificationFailed, `turnstile-failed: ${errorCodes}`);
}
}
@bindThis
public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
if (response == null) {
- throw new Error('testcaptcha-failed: no response provided');
+ throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'testcaptcha-failed: no response provided');
}
const success = response === 'testcaptcha-passed';
if (!success) {
- throw new Error('testcaptcha-failed');
+ throw new CaptchaError(captchaErrorCodes.verificationFailed, 'testcaptcha-failed');
+ }
+ }
+
+ @bindThis
+ public async get(): Promise<CaptchaSetting> {
+ const meta = await this.metaService.fetch(true);
+
+ let provider: CaptchaProvider;
+ switch (true) {
+ case meta.enableHcaptcha: {
+ provider = 'hcaptcha';
+ break;
+ }
+ case meta.enableMcaptcha: {
+ provider = 'mcaptcha';
+ break;
+ }
+ case meta.enableRecaptcha: {
+ provider = 'recaptcha';
+ break;
+ }
+ case meta.enableTurnstile: {
+ provider = 'turnstile';
+ break;
+ }
+ case meta.enableTestcaptcha: {
+ provider = 'testcaptcha';
+ break;
+ }
+ case meta.enableFC: {
+ provider = 'fc';
+ break;
+ }
+ default: {
+ provider = 'none';
+ break;
+ }
+ }
+
+ return {
+ provider: provider,
+ hcaptcha: {
+ siteKey: meta.hcaptchaSiteKey,
+ secretKey: meta.hcaptchaSecretKey,
+ },
+ mcaptcha: {
+ siteKey: meta.mcaptchaSitekey,
+ secretKey: meta.mcaptchaSecretKey,
+ instanceUrl: meta.mcaptchaInstanceUrl,
+ },
+ recaptcha: {
+ siteKey: meta.recaptchaSiteKey,
+ secretKey: meta.recaptchaSecretKey,
+ },
+ turnstile: {
+ siteKey: meta.turnstileSiteKey,
+ secretKey: meta.turnstileSecretKey,
+ },
+ fc: {
+ siteKey: meta.fcSiteKey,
+ secretKey: meta.fcSecretKey,
+ },
+ };
+ }
+
+ /**
+ * captchaの設定を更新します. その際、フロントエンド側で受け取ったcaptchaからの戻り値を検証し、passした場合のみ設定を更新します.
+ * 実際の検証処理はサービス内で定義されている各captchaプロバイダの検証関数に委譲します.
+ *
+ * @param provider 検証するcaptchaのプロバイダ
+ * @param params
+ * @param params.sitekey hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsitekey. それ以外のプロバイダでは無視されます
+ * @param params.secret hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsecret. それ以外のプロバイダでは無視されます
+ * @param params.instanceUrl mcaptchaの場合に指定するインスタンスのURL. それ以外のプロバイダでは無視されます
+ * @param params.captchaResult フロントエンド側で受け取ったcaptchaプロバイダからの戻り値. この値を使ってサーバサイドでの検証を行います
+ * @see verifyHcaptcha
+ * @see verifyMcaptcha
+ * @see verifyRecaptcha
+ * @see verifyTurnstile
+ * @see verifyTestcaptcha
+ */
+ @bindThis
+ public async save(
+ provider: CaptchaProvider,
+ params?: {
+ sitekey?: string | null;
+ secret?: string | null;
+ instanceUrl?: string | null;
+ captchaResult?: string | null;
+ },
+ ): Promise<CaptchaSaveResult> {
+ if (!supportedCaptchaProviders.includes(provider)) {
+ return {
+ success: false,
+ error: new CaptchaError(captchaErrorCodes.invalidProvider, `Invalid captcha provider: ${provider}`),
+ };
}
+
+ const operation = {
+ none: async () => {
+ await this.updateMeta(provider, params);
+ },
+ hcaptcha: async () => {
+ if (!params?.secret || !params.captchaResult) {
+ throw new CaptchaError(captchaErrorCodes.invalidParameters, 'hcaptcha-failed: secret and captureResult are required');
+ }
+
+ await this.verifyHcaptcha(params.secret, params.captchaResult);
+ await this.updateMeta(provider, params);
+ },
+ mcaptcha: async () => {
+ if (!params?.secret || !params.sitekey || !params.instanceUrl || !params.captchaResult) {
+ throw new CaptchaError(captchaErrorCodes.invalidParameters, 'mcaptcha-failed: secret, sitekey, instanceUrl and captureResult are required');
+ }
+
+ await this.verifyMcaptcha(params.secret, params.sitekey, params.instanceUrl, params.captchaResult);
+ await this.updateMeta(provider, params);
+ },
+ recaptcha: async () => {
+ if (!params?.secret || !params.captchaResult) {
+ throw new CaptchaError(captchaErrorCodes.invalidParameters, 'recaptcha-failed: secret and captureResult are required');
+ }
+
+ await this.verifyRecaptcha(params.secret, params.captchaResult);
+ await this.updateMeta(provider, params);
+ },
+ turnstile: async () => {
+ if (!params?.secret || !params.captchaResult) {
+ throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: secret and captureResult are required');
+ }
+
+ await this.verifyTurnstile(params.secret, params.captchaResult);
+ await this.updateMeta(provider, params);
+ },
+ testcaptcha: async () => {
+ if (!params?.captchaResult) {
+ throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: captureResult are required');
+ }
+
+ await this.verifyTestcaptcha(params.captchaResult);
+ await this.updateMeta(provider, params);
+ },
+ fc: async () => {
+ if (!params?.secret || !params.captchaResult) {
+ throw new CaptchaError(captchaErrorCodes.invalidParameters, 'frc-failed: secret and captureResult are required');
+ }
+
+ await this.verifyFriendlyCaptcha(params.captchaResult, params.captchaResult);
+ await this.updateMeta(provider, params);
+ },
+ }[provider];
+
+ return operation()
+ .then(() => ({ success: true }) as CaptchaSaveSuccess)
+ .catch(err => {
+ this.logger.info(err);
+ const error = err instanceof CaptchaError
+ ? err
+ : new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`);
+ return {
+ success: false,
+ error,
+ };
+ });
+ }
+
+ @bindThis
+ private async updateMeta(
+ provider: CaptchaProvider,
+ params?: {
+ sitekey?: string | null;
+ secret?: string | null;
+ instanceUrl?: string | null;
+ },
+ ) {
+ const metaPartial: Partial<
+ Pick<
+ MiMeta,
+ ('enableHcaptcha' | 'hcaptchaSiteKey' | 'hcaptchaSecretKey') |
+ ('enableMcaptcha' | 'mcaptchaSitekey' | 'mcaptchaSecretKey' | 'mcaptchaInstanceUrl') |
+ ('enableRecaptcha' | 'recaptchaSiteKey' | 'recaptchaSecretKey') |
+ ('enableTurnstile' | 'turnstileSiteKey' | 'turnstileSecretKey') |
+ ('enableTestcaptcha' | 'enableFC' | 'fcSiteKey' | 'fcSecretKey')
+ >
+ > = {
+ enableHcaptcha: provider === 'hcaptcha',
+ enableMcaptcha: provider === 'mcaptcha',
+ enableRecaptcha: provider === 'recaptcha',
+ enableTurnstile: provider === 'turnstile',
+ enableTestcaptcha: provider === 'testcaptcha',
+ enableFC: provider === 'fc',
+ };
+
+ const updateIfNotUndefined = <K extends keyof typeof metaPartial>(key: K, value: typeof metaPartial[K]) => {
+ if (value !== undefined) {
+ metaPartial[key] = value;
+ }
+ };
+ switch (provider) {
+ case 'hcaptcha': {
+ updateIfNotUndefined('hcaptchaSiteKey', params?.sitekey);
+ updateIfNotUndefined('hcaptchaSecretKey', params?.secret);
+ break;
+ }
+ case 'mcaptcha': {
+ updateIfNotUndefined('mcaptchaSitekey', params?.sitekey);
+ updateIfNotUndefined('mcaptchaSecretKey', params?.secret);
+ updateIfNotUndefined('mcaptchaInstanceUrl', params?.instanceUrl);
+ break;
+ }
+ case 'recaptcha': {
+ updateIfNotUndefined('recaptchaSiteKey', params?.sitekey);
+ updateIfNotUndefined('recaptchaSecretKey', params?.secret);
+ break;
+ }
+ case 'turnstile': {
+ updateIfNotUndefined('turnstileSiteKey', params?.sitekey);
+ updateIfNotUndefined('turnstileSecretKey', params?.secret);
+ break;
+ }
+ case 'fc': {
+ updateIfNotUndefined('fcSiteKey', params?.sitekey);
+ updateIfNotUndefined('fcSecretKey', params?.secret);
+ }
+ }
+
+ await this.metaService.update(metaPartial);
}
}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 141f905d7f..3c35dfc4ff 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -17,6 +17,8 @@ import { WebhookTestService } from '@/core/WebhookTestService.js';
import { FlashService } from '@/core/FlashService.js';
import { TimeService } from '@/core/TimeService.js';
import { EnvService } from '@/core/EnvService.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
+import { ApLogService } from '@/core/ApLogService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AnnouncementService } from './AnnouncementService.js';
@@ -166,6 +168,7 @@ const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisti
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService };
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
+const $ApLogService: Provider = { provide: 'ApLogService', useExisting: ApLogService };
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
@@ -232,6 +235,8 @@ const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpo
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
+const $TimeService: Provider = { provide: 'TimeService', useExisting: TimeService };
+const $EnvService: Provider = { provide: 'EnvService', useExisting: EnvService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
@@ -304,6 +309,7 @@ const $ApMentionService: Provider = { provide: 'ApMentionService', useExisting:
const $ApNoteService: Provider = { provide: 'ApNoteService', useExisting: ApNoteService };
const $ApPersonService: Provider = { provide: 'ApPersonService', useExisting: ApPersonService };
const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting: ApQuestionService };
+const $ApUtilityService: Provider = { provide: 'ApUtilityService', useExisting: ApUtilityService };
//#endregion
const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: SponsorsService };
@@ -320,6 +326,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
AccountUpdateService,
AnnouncementService,
AntennaService,
+ ApLogService,
AppLockService,
AchievementService,
AvatarDecorationService,
@@ -460,6 +467,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
ApNoteService,
ApPersonService,
ApQuestionService,
+ ApUtilityService,
QueueService,
SponsorsService,
@@ -472,6 +480,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$AccountUpdateService,
$AnnouncementService,
$AntennaService,
+ $ApLogService,
$AppLockService,
$AchievementService,
$AvatarDecorationService,
@@ -538,6 +547,8 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$ChannelFollowingService,
$RegistryApiService,
$ReversiService,
+ $TimeService,
+ $EnvService,
$ChartLoggerService,
$FederationChart,
@@ -610,6 +621,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$ApNoteService,
$ApPersonService,
$ApQuestionService,
+ $ApUtilityService,
//#endregion
$SponsorsService,
@@ -623,6 +635,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
AccountUpdateService,
AnnouncementService,
AntennaService,
+ ApLogService,
AppLockService,
AchievementService,
AvatarDecorationService,
@@ -762,6 +775,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
ApNoteService,
ApPersonService,
ApQuestionService,
+ ApUtilityService,
QueueService,
SponsorsService,
@@ -774,6 +788,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$AccountUpdateService,
$AnnouncementService,
$AntennaService,
+ $ApLogService,
$AppLockService,
$AchievementService,
$AvatarDecorationService,
@@ -839,6 +854,8 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$ChannelFollowingService,
$RegistryApiService,
$ReversiService,
+ $TimeService,
+ $EnvService,
$FederationChart,
$NotesChart,
@@ -910,6 +927,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$ApNoteService,
$ApPersonService,
$ApQuestionService,
+ $ApUtilityService,
//#endregion
$SponsorsService,
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index cc33fb5c0b..2e4eddf797 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -4,19 +4,18 @@
*/
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
-import { In, IsNull } from 'typeorm';
import * as Redis from 'ioredis';
-import { DI } from '@/di-symbols.js';
-import { IdService } from '@/core/IdService.js';
+import { In, IsNull } from 'typeorm';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
-import type { MiDriveFile } from '@/models/DriveFile.js';
-import type { MiEmoji } from '@/models/Emoji.js';
-import type { DriveFilesRepository, EmojisRepository, MiRole, MiUser } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
+import { DI } from '@/di-symbols.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
-import { UtilityService } from '@/core/UtilityService.js';
-import { query } from '@/misc/prelude/url.js';
+import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import type { DriveFilesRepository, EmojisRepository, MiRole, MiUser } from '@/models/_.js';
+import type { MiEmoji } from '@/models/Emoji.js';
import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Config } from '@/config.js';
@@ -24,6 +23,42 @@ import { DriveService } from './DriveService.js';
const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
+export const fetchEmojisHostTypes = [
+ 'local',
+ 'remote',
+ 'all',
+] as const;
+export type FetchEmojisHostTypes = typeof fetchEmojisHostTypes[number];
+export const fetchEmojisSortKeys = [
+ '+id',
+ '-id',
+ '+updatedAt',
+ '-updatedAt',
+ '+name',
+ '-name',
+ '+host',
+ '-host',
+ '+uri',
+ '-uri',
+ '+publicUrl',
+ '-publicUrl',
+ '+type',
+ '-type',
+ '+aliases',
+ '-aliases',
+ '+category',
+ '-category',
+ '+license',
+ '-license',
+ '+isSensitive',
+ '-isSensitive',
+ '+localOnly',
+ '-localOnly',
+ '+roleIdsThatCanBeUsedThisEmojiAsReaction',
+ '-roleIdsThatCanBeUsedThisEmojiAsReaction',
+] as const;
+export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number];
+
@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {
private emojisCache: MemoryKVCache<MiEmoji | null>;
@@ -32,16 +67,12 @@ export class CustomEmojiService implements OnApplicationShutdown {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
-
@Inject(DI.config)
private config: Config,
-
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
private utilityService: UtilityService,
private idService: IdService,
private emojiEntityService: EmojiEntityService,
@@ -67,7 +98,9 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis
public async add(data: {
- driveFile: MiDriveFile;
+ originalUrl: string;
+ publicUrl: string;
+ fileType: string;
name: string;
category: string | null;
aliases: string[];
@@ -84,9 +117,9 @@ export class CustomEmojiService implements OnApplicationShutdown {
category: data.category,
host: data.host,
aliases: data.aliases,
- originalUrl: data.driveFile.url,
- publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
- type: data.driveFile.webpublicType ?? data.driveFile.type,
+ originalUrl: data.originalUrl,
+ publicUrl: data.publicUrl,
+ type: data.fileType,
license: data.license,
isSensitive: data.isSensitive,
localOnly: data.localOnly,
@@ -115,7 +148,9 @@ export class CustomEmojiService implements OnApplicationShutdown {
public async update(data: (
{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
) & {
- driveFile?: MiDriveFile;
+ originalUrl?: string;
+ publicUrl?: string;
+ fileType?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
@@ -148,18 +183,22 @@ export class CustomEmojiService implements OnApplicationShutdown {
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,
+ originalUrl: data.originalUrl,
+ publicUrl: data.publicUrl,
+ type: data.fileType,
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
});
this.localEmojisCache.refresh();
- if (data.driveFile != null) {
- const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() });
- if (file && file.id !== data.driveFile.id) {
- await this.driveService.deleteFile(file, false, moderator ? moderator : undefined);
+ // If we're changing the file, then we need to delete the old one
+ if (data.originalUrl != null && data.originalUrl !== emoji.originalUrl) {
+ const oldFile = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() });
+ const newFile = await this.driveFilesRepository.findOneBy({ url: data.originalUrl, userHost: emoji.host ? emoji.host : IsNull() });
+
+ // But DON'T delete if this is the same file reference, otherwise we'll break the emoji!
+ if (oldFile && newFile && oldFile.id !== newFile.id) {
+ await this.driveService.deleteFile(oldFile, false, moderator ? moderator : undefined);
}
}
@@ -336,7 +375,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
- // クエリに使うホスト
+ // クエリに使うホスト
let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
: src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
: this.utilityService.isSelfHost(src) ? null // 自ホスト指定
@@ -445,6 +484,151 @@ export class CustomEmojiService implements OnApplicationShutdown {
}
@bindThis
+ public async fetchEmojis(
+ params?: {
+ query?: {
+ updatedAtFrom?: string;
+ updatedAtTo?: string;
+ name?: string;
+ host?: string;
+ uri?: string;
+ publicUrl?: string;
+ type?: string;
+ aliases?: string;
+ category?: string;
+ license?: string;
+ isSensitive?: boolean;
+ localOnly?: boolean;
+ hostType?: FetchEmojisHostTypes;
+ roleIds?: string[];
+ },
+ sinceId?: string;
+ untilId?: string;
+ },
+ opts?: {
+ limit?: number;
+ page?: number;
+ sortKeys?: FetchEmojisSortKeys[]
+ },
+ ) {
+ function multipleWordsToQuery(words: string) {
+ return words.split(/\s/).filter(x => x.length > 0).map(x => `%${sqlLikeEscape(x)}%`);
+ }
+
+ const builder = this.emojisRepository.createQueryBuilder('emoji');
+ if (params?.query) {
+ const q = params.query;
+ if (q.updatedAtFrom) {
+ // noIndexScan
+ builder.andWhere('CAST(emoji.updatedAt AS DATE) >= :updatedAtFrom', { updatedAtFrom: q.updatedAtFrom });
+ }
+ if (q.updatedAtTo) {
+ // noIndexScan
+ builder.andWhere('CAST(emoji.updatedAt AS DATE) <= :updatedAtTo', { updatedAtTo: q.updatedAtTo });
+ }
+ if (q.name) {
+ builder.andWhere('emoji.name ~~ ANY(ARRAY[:...name])', { name: multipleWordsToQuery(q.name) });
+ }
+
+ switch (true) {
+ case q.hostType === 'local': {
+ builder.andWhere('emoji.host IS NULL');
+ break;
+ }
+ case q.hostType === 'remote': {
+ if (q.host) {
+ // noIndexScan
+ builder.andWhere('emoji.host ~~ ANY(ARRAY[:...host])', { host: multipleWordsToQuery(q.host) });
+ } else {
+ builder.andWhere('emoji.host IS NOT NULL');
+ }
+ break;
+ }
+ }
+
+ if (q.uri) {
+ // noIndexScan
+ builder.andWhere('emoji.uri ~~ ANY(ARRAY[:...uri])', { uri: multipleWordsToQuery(q.uri) });
+ }
+ if (q.publicUrl) {
+ // noIndexScan
+ builder.andWhere('emoji.publicUrl ~~ ANY(ARRAY[:...publicUrl])', { publicUrl: multipleWordsToQuery(q.publicUrl) });
+ }
+ if (q.type) {
+ // noIndexScan
+ builder.andWhere('emoji.type ~~ ANY(ARRAY[:...type])', { type: multipleWordsToQuery(q.type) });
+ }
+ if (q.aliases) {
+ // noIndexScan
+ const subQueryBuilder = builder.subQuery()
+ .select('COUNT(0)', 'count')
+ .from(
+ sq2 => sq2
+ .select('unnest(subEmoji.aliases)', 'alias')
+ .addSelect('subEmoji.id', 'id')
+ .from('emoji', 'subEmoji'),
+ 'aliasTable',
+ )
+ .where('"emoji"."id" = "aliasTable"."id"')
+ .andWhere('"aliasTable"."alias" ~~ ANY(ARRAY[:...aliases])', { aliases: multipleWordsToQuery(q.aliases) });
+
+ builder.andWhere(`(${subQueryBuilder.getQuery()}) > 0`);
+ }
+ if (q.category) {
+ builder.andWhere('emoji.category ~~ ANY(ARRAY[:...category])', { category: multipleWordsToQuery(q.category) });
+ }
+ if (q.license) {
+ // noIndexScan
+ builder.andWhere('emoji.license ~~ ANY(ARRAY[:...license])', { license: multipleWordsToQuery(q.license) });
+ }
+ if (q.isSensitive != null) {
+ // noIndexScan
+ builder.andWhere('emoji.isSensitive = :isSensitive', { isSensitive: q.isSensitive });
+ }
+ if (q.localOnly != null) {
+ // noIndexScan
+ builder.andWhere('emoji.localOnly = :localOnly', { localOnly: q.localOnly });
+ }
+ if (q.roleIds && q.roleIds.length > 0) {
+ builder.andWhere('emoji.roleIdsThatCanBeUsedThisEmojiAsReaction && ARRAY[:...roleIds]::VARCHAR[]', { roleIds: q.roleIds });
+ }
+ }
+
+ if (params?.sinceId) {
+ builder.andWhere('emoji.id > :sinceId', { sinceId: params.sinceId });
+ }
+ if (params?.untilId) {
+ builder.andWhere('emoji.id < :untilId', { untilId: params.untilId });
+ }
+
+ if (opts?.sortKeys && opts.sortKeys.length > 0) {
+ for (const sortKey of opts.sortKeys) {
+ const direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
+ const key = sortKey.replace(/^[+-]/, '');
+ builder.addOrderBy(`emoji.${key}`, direction);
+ }
+ } else {
+ builder.addOrderBy('emoji.id', 'DESC');
+ }
+
+ const limit = opts?.limit ?? 10;
+ if (opts?.page) {
+ builder.skip((opts.page - 1) * limit);
+ }
+
+ builder.take(limit);
+
+ const [emojis, count] = await builder.getManyAndCount();
+
+ return {
+ emojis,
+ count: (count > limit ? emojis.length : count),
+ allCount: count,
+ allPages: Math.ceil(count / limit),
+ };
+ }
+
+ @bindThis
public dispose(): void {
this.emojisCache.dispose();
}
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index 086f2f94d5..a65059b417 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -37,6 +37,7 @@ import { InternalStorageService } from '@/core/InternalStorageService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FileInfoService } from '@/core/FileInfoService.js';
+import type { FileInfo } from '@/core/FileInfoService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js';
@@ -139,15 +140,18 @@ export class DriveService {
/***
* Save file
+ * @param file
* @param path Path for original
* @param name Name for original (should be extention corrected)
- * @param type Content-Type for original
- * @param hash Hash for original
- * @param size Size for original
+ * @param info File metadata
*/
@bindThis
- private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<MiDriveFile> {
- // thunbnail, webpublic を必要なら生成
+ private async save(file: MiDriveFile, path: string, name: string, info: FileInfo): Promise<MiDriveFile> {
+ const type = info.type.mime;
+ const hash = info.md5;
+ const size = info.size;
+
+ // thunbnail, webpublic を必要なら生成
const alts = await this.generateAlts(path, type, !file.uri);
if (this.meta.useObjectStorage) {
@@ -223,9 +227,11 @@ export class DriveService {
return await this.driveFilesRepository.insertOne(file);
} else { // use internal storage
- const accessKey = randomUUID();
- const thumbnailAccessKey = 'thumbnail-' + randomUUID();
- const webpublicAccessKey = 'webpublic-' + randomUUID();
+ const ext = FILE_TYPE_BROWSERSAFE.includes(type) ? info.type.ext : null;
+
+ const accessKey = makeFileKey(ext);
+ const thumbnailAccessKey = makeFileKey(ext, 'thumbnail');
+ const webpublicAccessKey = makeFileKey(ext, 'webpublic');
// Ugly type is just to help TS figure out that 2nd / 3rd promises are optional.
const promises: [Promise<string>, ...(Promise<string> | undefined)[]] = [
@@ -514,7 +520,7 @@ export class DriveService {
// If usage limit exceeded
if (driveCapacity < usage + info.size) {
if (isLocalUser) {
- throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.');
+ throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.', true);
}
await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as MiRemoteUser, driveCapacity - info.size);
}
@@ -616,7 +622,7 @@ export class DriveService {
}
}
} else {
- file = await (this.save(file, path, detectedName, info.type.mime, info.md5, info.size));
+ file = await (this.save(file, path, detectedName, info));
}
this.registerLogger.succ(`drive file has been created ${file.id}`);
@@ -862,3 +868,16 @@ export class DriveService {
}
}
}
+
+function makeFileKey(ext: string | null, prefix?: string): string {
+ const parts: string[] = [randomUUID()];
+
+ if (prefix) {
+ parts.unshift(prefix, '-');
+ }
+ if (ext) {
+ parts.push('.', ext);
+ }
+
+ return parts.join('');
+}
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index fca3ad847a..3f7ed99348 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis';
+import { QueryFailedError } from 'typeorm';
import type { InstancesRepository } from '@/models/_.js';
import type { MiInstance } from '@/models/Instance.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
@@ -12,7 +13,6 @@ import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
-import { QueryFailedError } from 'typeorm';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
@Injectable()
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index 987999bce7..ce3af7c774 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -181,7 +181,7 @@ export class FetchInstanceMetadataService {
}
@bindThis
- private async fetchDom(instance: MiInstance): Promise<DOMWindow['document']> {
+ private async fetchDom(instance: MiInstance): Promise<Document> {
this.logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host;
@@ -206,7 +206,7 @@ export class FetchInstanceMetadataService {
}
@bindThis
- private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise<string | null> {
+ private async fetchFaviconUrl(instance: MiInstance, doc: Document | null): Promise<string | null> {
const url = 'https://' + instance.host;
if (doc) {
@@ -232,7 +232,7 @@ export class FetchInstanceMetadataService {
}
@bindThis
- private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
+ private async fetchIconUrl(instance: MiInstance, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> {
if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
const url = 'https://' + instance.host;
return (new URL(manifest.icons[0].src, url)).href;
@@ -261,7 +261,7 @@ export class FetchInstanceMetadataService {
}
@bindThis
- private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
+ private async getThemeColor(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> {
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
if (themeColor) {
@@ -273,7 +273,7 @@ export class FetchInstanceMetadataService {
}
@bindThis
- private async getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
+ private async getSiteName(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> {
if (info && info.metadata) {
if (typeof info.metadata.nodeName === 'string') {
return info.metadata.nodeName;
@@ -298,7 +298,7 @@ export class FetchInstanceMetadataService {
}
@bindThis
- private async getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
+ private async getDescription(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> {
if (info && info.metadata) {
if (typeof info.metadata.nodeDescription === 'string') {
return info.metadata.nodeDescription;
diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts
index 083153940a..19992a7597 100644
--- a/packages/backend/src/core/HttpRequestService.ts
+++ b/packages/backend/src/core/HttpRequestService.ts
@@ -16,8 +16,8 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
-import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
-import type { IObject } from '@/core/activitypub/type.js';
+import { IObject } from '@/core/activitypub/type.js';
+import { ApUtilityService } from './activitypub/ApUtilityService.js';
import type { Response } from 'node-fetch';
import type { URL } from 'node:url';
@@ -145,6 +145,7 @@ export class HttpRequestService {
constructor(
@Inject(DI.config)
private config: Config,
+ private readonly apUtilityService: ApUtilityService,
) {
const cache = new CacheableLookup({
maxTtl: 3600, // 1hours
@@ -198,6 +199,7 @@ export class HttpRequestService {
* Get agent by URL
* @param url URL
* @param bypassProxy Allways bypass proxy
+ * @param isLocalAddressAllowed
*/
@bindThis
public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent {
@@ -229,10 +231,11 @@ export class HttpRequestService {
validators: [validateContentTypeSetAsActivityPub],
});
- const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
- assertActivityMatchesUrls(activity, [finalUrl]);
+ // Make sure the object ID matches the final URL (which is where it actually exists).
+ // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
+ this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
return activity;
}
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 42676d6f98..6c2f673217 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -179,6 +179,40 @@ export class MfmService {
break;
}
+ // this is here only to catch upstream changes!
+ case 'ruby--': {
+ let ruby: [string, string][] = [];
+ for (const child of node.childNodes) {
+ if (child.nodeName === 'rp') {
+ continue;
+ }
+ if (treeAdapter.isTextNode(child) && !/\s|\[|\]/.test(child.value)) {
+ ruby.push([child.value, '']);
+ continue;
+ }
+ if (child.nodeName === 'rt' && ruby.length > 0) {
+ const rt = getText(child);
+ if (/\s|\[|\]/.test(rt)) {
+ // If any space is included in rt, it is treated as a normal text
+ ruby = [];
+ appendChildren(node.childNodes);
+ break;
+ } else {
+ ruby.at(-1)![1] = rt;
+ continue;
+ }
+ }
+ // If any other element is included in ruby, it is treated as a normal text
+ ruby = [];
+ appendChildren(node.childNodes);
+ break;
+ }
+ for (const [base, rt] of ruby) {
+ text += `$[ruby ${base} ${rt}]`;
+ }
+ break;
+ }
+
// block code (<pre><code>)
case 'pre': {
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
@@ -230,6 +264,75 @@ export class MfmService {
break;
}
+ case 'rp': break;
+ case 'rt': {
+ appendChildren(node.childNodes);
+ break;
+ }
+ case 'ruby': {
+ if (node.childNodes) {
+ /*
+ we get:
+ ```
+ <ruby>
+ some text <rp>(</rp> <rt>annotation</rt> <rp>)</rp>
+ more text <rt>more annotation<rt>
+ </ruby>
+ ```
+
+ and we want to produce:
+ ```
+ $[ruby $[group some text] annotation]
+ $[ruby $[group more text] more annotation]
+ ```
+
+ that `group` is a hack, because when the `ruby` render
+ sees just text inside the `$[ruby]`, it splits on
+ whitespace, considers the first "word" to be the main
+ content, and the rest the annotation
+
+ with that `group`, we force it to consider the whole
+ group as the main content
+
+ (note that the `rp` are to be ignored, they only exist
+ for browsers who don't understand ruby)
+ */
+ let nonRtNodes = [];
+ // scan children, ignore `rp`, split on `rt`
+ for (const child of node.childNodes) {
+ if (treeAdapter.isTextNode(child)) {
+ nonRtNodes.push(child);
+ continue;
+ }
+ if (!treeAdapter.isElementNode(child)) {
+ continue;
+ }
+ if (child.nodeName === 'rp') {
+ continue;
+ }
+ if (child.nodeName === 'rt') {
+ // the only case in which we don't need a `$[group ]`
+ // is when both sides of the ruby are simple words
+ const needsGroup = nonRtNodes.length > 1 ||
+ /\s|\[|\]/.test(getText(nonRtNodes[0])) ||
+ /\s|\[|\]/.test(getText(child));
+ text += '$[ruby ';
+ if (needsGroup) text += '$[group ';
+ appendChildren(nonRtNodes);
+ if (needsGroup) text += ']';
+ text += ' ';
+ analyze(child);
+ text += ']';
+ nonRtNodes = [];
+ continue;
+ }
+ nonRtNodes.push(child);
+ }
+ appendChildren(nonRtNodes);
+ }
+ break;
+ }
+
default: // includes inline elements
{
appendChildren(node.childNodes);
@@ -348,6 +451,14 @@ export class MfmService {
}
}
+ // hack for ruby, should never be needed because we should
+ // never send this out to other instances
+ case 'group': {
+ const el = doc.createElement('span');
+ appendChildren(node.children, el);
+ return el;
+ }
+
default: {
return fnDefault(node);
}
@@ -526,11 +637,65 @@ export class MfmService {
},
async fn(node) {
- const el = doc.createElement('span');
- el.textContent = '*';
- await appendChildren(node.children, el);
- el.textContent += '*';
- return el;
+ switch (node.props.name) {
+ case 'group': { // hack for ruby
+ const el = doc.createElement('span');
+ await appendChildren(node.children, el);
+ return el;
+ }
+ case 'ruby': {
+ if (node.children.length === 1) {
+ const child = node.children[0];
+ const text = child.type === 'text' ? child.props.text : '';
+ const rubyEl = doc.createElement('ruby');
+ const rtEl = doc.createElement('rt');
+
+ const rpStartEl = doc.createElement('rp');
+ rpStartEl.appendChild(doc.createTextNode('('));
+ const rpEndEl = doc.createElement('rp');
+ rpEndEl.appendChild(doc.createTextNode(')'));
+
+ rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
+ rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
+ rubyEl.appendChild(rpStartEl);
+ rubyEl.appendChild(rtEl);
+ rubyEl.appendChild(rpEndEl);
+ return rubyEl;
+ } else {
+ const rt = node.children.at(-1);
+
+ if (!rt) {
+ const el = doc.createElement('span');
+ await appendChildren(node.children, el);
+ return el;
+ }
+
+ const text = rt.type === 'text' ? rt.props.text : '';
+ const rubyEl = doc.createElement('ruby');
+ const rtEl = doc.createElement('rt');
+
+ const rpStartEl = doc.createElement('rp');
+ rpStartEl.appendChild(doc.createTextNode('('));
+ const rpEndEl = doc.createElement('rp');
+ rpEndEl.appendChild(doc.createTextNode(')'));
+
+ await appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
+ rtEl.appendChild(doc.createTextNode(text.trim()));
+ rubyEl.appendChild(rpStartEl);
+ rubyEl.appendChild(rtEl);
+ rubyEl.appendChild(rpEndEl);
+ return rubyEl;
+ }
+ }
+
+ default: {
+ const el = doc.createElement('span');
+ el.textContent = '*';
+ await appendChildren(node.children, el);
+ el.textContent += '*';
+ return el;
+ }
+ }
},
blockCode(node) {
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 96bb30a0d6..df31cb4247 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -144,6 +144,7 @@ type Option = {
uri?: string | null;
url?: string | null;
app?: MiApp | null;
+ processErrors?: string[] | null;
};
export type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] });
@@ -228,7 +229,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- public async create(user: {
+ public async create(user: MiUser & {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
@@ -309,6 +310,9 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
+ // Check quote permissions
+ await this.checkQuotePermissions(data, user);
+
// Check blocking
if (this.isRenote(data) && !this.isQuote(data)) {
if (data.renote.userHost === null) {
@@ -435,7 +439,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- public async import(user: {
+ public async import(user: MiUser & {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
@@ -482,14 +486,15 @@ export class NoteCreateService implements OnApplicationShutdown {
renoteUserId: data.renote ? data.renote.userId : null,
renoteUserHost: data.renote ? data.renote.userHost : null,
userHost: user.host,
+ processErrors: data.processErrors,
});
// should really not happen, but better safe than sorry
if (data.reply?.id === insert.id) {
- throw new Error("A note can't reply to itself");
+ throw new Error('A note can\'t reply to itself');
}
if (data.renote?.id === insert.id) {
- throw new Error("A note can't renote itself");
+ throw new Error('A note can\'t renote itself');
}
if (data.uri != null) insert.uri = data.uri;
@@ -552,7 +557,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- private async postNoteCreated(note: MiNote, user: {
+ private async postNoteCreated(note: MiNote, user: MiUser & {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
@@ -678,14 +683,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.roleService.addNoteToRoleTimeline(noteObj);
- this.webhookService.getActiveWebhooks().then(webhooks => {
- webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'note', {
- note: noteObj,
- });
- }
- });
+ this.webhookService.enqueueUserWebhook(user.id, 'note', { note: noteObj });
const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
@@ -717,13 +715,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!isThreadMuted && !muted) {
nm.push(data.reply.userId, 'reply');
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'reply', {
- note: noteObj,
- });
- }
+ this.webhookService.enqueueUserWebhook(data.reply.userId, 'reply', { note: noteObj });
}
}
}
@@ -757,22 +749,16 @@ export class NoteCreateService implements OnApplicationShutdown {
// Publish event
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'renote', {
- note: noteObj,
- });
- }
+ this.webhookService.enqueueUserWebhook(data.renote.userId, 'renote', { note: noteObj });
}
}
nm.notify();
//#region AP deliver
- if (this.userEntityService.isLocalUser(user)) {
+ if (!data.localOnly && this.userEntityService.isLocalUser(user)) {
(async () => {
- const noteActivity = await this.renderNoteOrRenoteActivity(data, note);
+ const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user);
const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity);
// メンションされたリモートユーザーに配送
@@ -905,13 +891,7 @@ export class NoteCreateService implements OnApplicationShutdown {
});
this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'mention', {
- note: detailPackedNote,
- });
- }
+ this.webhookService.enqueueUserWebhook(u.id, 'mention', { note: detailPackedNote });
// Create notification
nm.push(u.id, 'mention');
@@ -924,12 +904,12 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
+ private async renderNoteOrRenoteActivity(data: Option, note: MiNote, user: MiUser) {
if (data.localOnly) return null;
const content = this.isRenote(data) && !this.isQuote(data)
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
- : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
+ : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, user, false), note);
return this.apRendererService.addContext(content);
}
@@ -1172,4 +1152,29 @@ export class NoteCreateService implements OnApplicationShutdown {
public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
await this.dispose();
}
+
+ @bindThis
+ public async checkQuotePermissions(data: Option, user: MiUser): Promise<void> {
+ // Not a quote
+ if (!this.isRenote(data) || !this.isQuote(data)) return;
+
+ // User cannot quote
+ if (user.rejectQuotes) {
+ if (user.host == null) {
+ throw new IdentifiableError('1c0ea108-d1e3-4e8e-aa3f-4d2487626153', 'QUOTE_DISABLED_FOR_USER');
+ } else {
+ (data as Option).renote = null;
+ (data.processErrors ??= []).push('quoteUnavailable');
+ }
+ }
+
+ // Instance cannot quote
+ if (user.host) {
+ const instance = await this.federatedInstanceService.fetch(user.host);
+ if (instance?.rejectQuotes) {
+ (data as Option).renote = null;
+ (data.processErrors ??= []).push('quoteUnavailable');
+ }
+ }
+ }
}
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index b51a3143c9..1f94e65809 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -24,9 +24,14 @@ import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { LatestNoteService } from '@/core/LatestNoteService.js';
+import { ApLogService } from '@/core/ApLogService.js';
+import Logger from '@/logger.js';
+import { LoggerService } from './LoggerService.js';
@Injectable()
export class NoteDeleteService {
+ private readonly logger: Logger;
+
constructor(
@Inject(DI.config)
private config: Config,
@@ -55,7 +60,11 @@ export class NoteDeleteService {
private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart,
private latestNoteService: LatestNoteService,
- ) {}
+ private readonly apLogService: ApLogService,
+ loggerService: LoggerService,
+ ) {
+ this.logger = loggerService.getLogger('note-delete-service');
+ }
/**
* 投稿を削除します。
@@ -153,9 +162,13 @@ export class NoteDeleteService {
noteUserId: note.userId,
noteUserUsername: user.username,
noteUserHost: user.host,
- note: note,
});
}
+
+ if (note.uri) {
+ this.apLogService.deleteObjectLogs(note.uri)
+ .catch(err => this.logger.error(err, `Failed to delete AP logs for note '${note.uri}'`));
+ }
}
@bindThis
diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts
index f1c7bcbea5..7851af86b7 100644
--- a/packages/backend/src/core/NoteEditService.ts
+++ b/packages/backend/src/core/NoteEditService.ts
@@ -140,6 +140,7 @@ type Option = {
app?: MiApp | null;
updatedAt?: Date | null;
editcount?: boolean | null;
+ processErrors?: string[] | null;
};
@Injectable()
@@ -224,7 +225,7 @@ export class NoteEditService implements OnApplicationShutdown {
}
@bindThis
- public async edit(user: {
+ public async edit(user: MiUser & {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
@@ -309,7 +310,7 @@ export class NoteEditService implements OnApplicationShutdown {
if (this.isRenote(data)) {
if (data.renote.id === oldnote.id) {
- throw new Error("A note can't renote itself");
+ throw new Error('A note can\'t renote itself');
}
switch (data.renote.visibility) {
@@ -337,6 +338,9 @@ export class NoteEditService implements OnApplicationShutdown {
}
}
+ // Check quote permissions
+ await this.noteCreateService.checkQuotePermissions(data, user);
+
// Check blocking
if (this.isRenote(data) && !this.isQuote(data)) {
if (data.renote.userHost === null) {
@@ -529,6 +533,7 @@ export class NoteEditService implements OnApplicationShutdown {
if (data.uri != null) note.uri = data.uri;
if (data.url != null) note.url = data.url;
+ if (data.processErrors !== undefined) note.processErrors = data.processErrors;
if (mentionedUsers.length > 0) {
note.mentions = mentionedUsers.map(u => u.id);
@@ -584,7 +589,7 @@ export class NoteEditService implements OnApplicationShutdown {
}
@bindThis
- private async postNoteEdited(note: MiNote, oldNote: MiNote, user: {
+ private async postNoteEdited(note: MiNote, oldNote: MiNote, user: MiUser & {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
@@ -664,14 +669,7 @@ export class NoteEditService implements OnApplicationShutdown {
this.roleService.addNoteToRoleTimeline(noteObj);
- this.webhookService.getActiveWebhooks().then(webhooks => {
- webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'note', {
- note: noteObj,
- });
- }
- });
+ this.webhookService.enqueueUserWebhook(user.id, 'note', { note: noteObj });
const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
@@ -700,12 +698,7 @@ export class NoteEditService implements OnApplicationShutdown {
nm.push(data.reply.userId, 'edited');
this.globalEventService.publishMainStream(data.reply.userId, 'edited', noteObj);
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('edited'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'edited', {
- note: noteObj,
- });
- }
+ this.webhookService.enqueueUserWebhook(data.reply.userId, 'reply', { note: noteObj });
}
}
}
@@ -713,9 +706,9 @@ export class NoteEditService implements OnApplicationShutdown {
nm.notify();
//#region AP deliver
- if (this.userEntityService.isLocalUser(user)) {
+ if (!data.localOnly && this.userEntityService.isLocalUser(user)) {
(async () => {
- const noteActivity = await this.renderNoteOrRenoteActivity(data, note);
+ const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user);
const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity);
// メンションされたリモートユーザーに配送
@@ -810,6 +803,7 @@ export class NoteEditService implements OnApplicationShutdown {
(note.files != null && note.files.length > 0);
}
+ // TODO why is this unused?
@bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
@@ -837,13 +831,7 @@ export class NoteEditService implements OnApplicationShutdown {
});
this.globalEventService.publishMainStream(u.id, 'edited', detailPackedNote);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('edited'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'edited', {
- note: detailPackedNote,
- });
- }
+ this.webhookService.enqueueUserWebhook(u.id, 'edited', { note: detailPackedNote });
// Create notification
nm.push(u.id, 'edited');
@@ -851,14 +839,12 @@ export class NoteEditService implements OnApplicationShutdown {
}
@bindThis
- private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
+ private async renderNoteOrRenoteActivity(data: Option, note: MiNote, user: MiUser) {
if (data.localOnly) return null;
- const user = await this.usersRepository.findOneBy({ id: note.userId });
- if (user == null) throw new Error('user not found');
const content = this.isRenote(data) && !this.isQuote(data)
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
- : this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, false), user);
+ : this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, user, false), user);
return this.apRendererService.addContext(content);
}
diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts
index 6c96ab16cf..d6364613bd 100644
--- a/packages/backend/src/core/PollService.ts
+++ b/packages/backend/src/core/PollService.ts
@@ -100,7 +100,7 @@ export class PollService {
if (user == null) throw new Error('note not found');
if (this.userEntityService.isLocalUser(user)) {
- const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
+ const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, user, false), user));
this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content);
}
diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts
index bb2a463354..37721d2bf1 100644
--- a/packages/backend/src/core/S3Service.ts
+++ b/packages/backend/src/core/S3Service.ts
@@ -28,7 +28,7 @@ export class S3Service {
? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
- const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
+ const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy, true);
const handlerOption: NodeHttpHandlerOptions = {};
if (meta.objectStorageUseSSL) {
handlerOption.httpsAgent = agent as https.Agent;
diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts
index 6dc3e85fc8..4782a6c7b0 100644
--- a/packages/backend/src/core/SearchService.ts
+++ b/packages/backend/src/core/SearchService.ts
@@ -6,16 +6,17 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { Config } from '@/config.js';
+import { type Config, FulltextSearchProvider } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { MiNote } from '@/models/Note.js';
-import { MiUser } from '@/models/_.js';
import type { NotesRepository } from '@/models/_.js';
+import { MiUser } from '@/models/_.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { CacheService } from '@/core/CacheService.js';
import { QueryService } from '@/core/QueryService.js';
import { IdService } from '@/core/IdService.js';
+import { LoggerService } from '@/core/LoggerService.js';
import type { Index, MeiliSearch } from 'meilisearch';
type K = string;
@@ -27,12 +28,81 @@ type Q =
{ op: '<', k: K, v: number } |
{ op: '>=', k: K, v: number } |
{ op: '<=', k: K, v: number } |
- { op: 'is null', k: K} |
- { op: 'is not null', k: K} |
+ { op: 'is null', k: K } |
+ { op: 'is not null', k: K } |
{ op: 'and', qs: Q[] } |
{ op: 'or', qs: Q[] } |
{ op: 'not', q: Q };
+const fileTypes = {
+ image: [
+ 'image/webp',
+ 'image/png',
+ 'image/jpeg',
+ 'image/avif',
+ 'image/apng',
+ 'image/gif',
+ ],
+ video: [
+ 'video/mp4',
+ 'video/webm',
+ 'video/mpeg',
+ 'video/x-m4v',
+ ],
+ audio: [
+ 'audio/mpeg',
+ 'audio/flac',
+ 'audio/wav',
+ 'audio/aac',
+ 'audio/webm',
+ 'audio/opus',
+ 'audio/ogg',
+ 'audio/x-m4a',
+ 'audio/mod',
+ 'audio/s3m',
+ 'audio/xm',
+ 'audio/it',
+ 'audio/x-mod',
+ 'audio/x-s3m',
+ 'audio/x-xm',
+ 'audio/x-it',
+ ],
+ // Keep in sync with frontend-shared/js/const.ts
+ module: [
+ 'audio/mod',
+ 'audio/x-mod',
+ 'audio/s3m',
+ 'audio/x-s3m',
+ 'audio/xm',
+ 'audio/x-xm',
+ 'audio/it',
+ 'audio/x-it',
+ ],
+ flash: [
+ 'application/x-shockwave-flash',
+ 'application/vnd.adobe.flash.movie',
+ ],
+};
+
+// Make sure to regenerate misskey-js and check search.note.vue after changing these
+export const fileTypeCategories = ['image', 'video', 'audio', 'module', 'flash', null] as const;
+export type FileTypeCategory = typeof fileTypeCategories[number];
+
+export type SearchOpts = {
+ userId?: MiNote['userId'] | null;
+ channelId?: MiNote['channelId'] | null;
+ host?: string | null;
+ filetype?: FileTypeCategory;
+ order?: string | null;
+ disableMeili?: boolean | null;
+};
+
+export type SearchPagination = {
+ untilId?: MiNote['id'];
+ sinceId?: MiNote['id'];
+ limit: number;
+};
+
function compileValue(value: V): string {
if (typeof value === 'string') {
return `'${value}'`; // TODO: escape
@@ -64,7 +134,8 @@ function compileQuery(q: Q): string {
@Injectable()
export class SearchService {
private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local';
- private meilisearchNoteIndex: Index | null = null;
+ private readonly meilisearchNoteIndex: Index | null = null;
+ private readonly provider: FulltextSearchProvider;
constructor(
@Inject(DI.config)
@@ -79,6 +150,7 @@ export class SearchService {
private cacheService: CacheService,
private queryService: QueryService,
private idService: IdService,
+ private loggerService: LoggerService,
) {
if (meilisearch) {
this.meilisearchNoteIndex = meilisearch.index(`${this.config.meilisearch?.index}---notes`);
@@ -110,189 +182,198 @@ export class SearchService {
if (this.config.meilisearch?.scope) {
this.meilisearchIndexScope = this.config.meilisearch.scope;
}
+
+ this.provider = config.fulltextSearch?.provider ?? 'sqlLike';
+ this.loggerService.getLogger('SearchService').info(`-- Provider: ${this.provider}`);
}
@bindThis
public async indexNote(note: MiNote): Promise<void> {
+ if (!this.meilisearch) return;
if (note.text == null && note.cw == null) return;
if (!['home', 'public'].includes(note.visibility)) return;
- if (this.meilisearch) {
- switch (this.meilisearchIndexScope) {
- case 'global':
- break;
+ switch (this.meilisearchIndexScope) {
+ case 'global':
+ break;
- case 'local':
- if (note.userHost == null) break;
- return;
+ case 'local':
+ if (note.userHost == null) break;
+ return;
- default: {
- if (note.userHost == null) break;
- if (this.meilisearchIndexScope.includes(note.userHost)) break;
- return;
- }
+ default: {
+ if (note.userHost == null) break;
+ if (this.meilisearchIndexScope.includes(note.userHost)) break;
+ return;
}
-
- await this.meilisearchNoteIndex?.addDocuments([{
- id: note.id,
- createdAt: this.idService.parse(note.id).date.getTime(),
- userId: note.userId,
- userHost: note.userHost,
- channelId: note.channelId,
- cw: note.cw,
- text: note.text,
- tags: note.tags,
- attachedFileTypes: note.attachedFileTypes,
- }], {
- primaryKey: 'id',
- });
}
+
+ await this.meilisearchNoteIndex?.addDocuments([{
+ id: note.id,
+ createdAt: this.idService.parse(note.id).date.getTime(),
+ userId: note.userId,
+ userHost: note.userHost,
+ channelId: note.channelId,
+ cw: note.cw,
+ text: note.text,
+ tags: note.tags,
+ attachedFileTypes: note.attachedFileTypes,
+ }], {
+ primaryKey: 'id',
+ });
}
@bindThis
public async unindexNote(note: MiNote): Promise<void> {
+ if (!this.meilisearch) return;
if (!['home', 'public'].includes(note.visibility)) return;
- if (this.meilisearch) {
- this.meilisearchNoteIndex!.deleteDocument(note.id);
- }
+ await this.meilisearchNoteIndex?.deleteDocument(note.id);
}
@bindThis
- public async searchNote(q: string, me: MiUser | null, opts: {
- userId?: MiNote['userId'] | null;
- channelId?: MiNote['channelId'] | null;
- host?: string | null;
- filetype?: string | null;
- order?: string | null;
- disableMeili?: boolean | null;
- }, pagination: {
- untilId?: MiNote['id'];
- sinceId?: MiNote['id'];
- limit?: number;
- }): Promise<MiNote[]> {
- if (this.meilisearch && !opts.disableMeili) {
- const filter: Q = {
- op: 'and',
- qs: [],
- };
- if (pagination.untilId) filter.qs.push({ op: '<', k: 'createdAt', v: this.idService.parse(pagination.untilId).date.getTime() });
- if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() });
- if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
- if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
- if (opts.host) {
- if (opts.host === '.') {
- filter.qs.push({ op: 'is null', k: 'userHost' });
- } else {
- filter.qs.push({ op: '=', k: 'userHost', v: opts.host });
- }
+ public async searchNote(
+ q: string,
+ me: MiUser | null,
+ opts: SearchOpts,
+ pagination: SearchPagination,
+ ): Promise<MiNote[]> {
+ switch (this.provider) {
+ case 'sqlLike':
+ case 'sqlPgroonga':
+ case 'sqlTsvector': {
+ // ほとんど内容に差がないのでsqlLikeとsqlPgroongaを同じ処理にしている.
+ // 今後の拡張で差が出る用であれば関数を分ける.
+ return this.searchNoteByLike(q, me, opts, pagination);
}
- if (opts.filetype) {
- if (opts.filetype === 'image') {
- filter.qs.push({ op: 'or', qs: [
- { op: '=', k: 'attachedFileTypes', v: 'image/webp' },
- { op: '=', k: 'attachedFileTypes', v: 'image/png' },
- { op: '=', k: 'attachedFileTypes', v: 'image/jpeg' },
- { op: '=', k: 'attachedFileTypes', v: 'image/avif' },
- { op: '=', k: 'attachedFileTypes', v: 'image/apng' },
- { op: '=', k: 'attachedFileTypes', v: 'image/gif' },
- ] });
- } else if (opts.filetype === 'video') {
- filter.qs.push({ op: 'or', qs: [
- { op: '=', k: 'attachedFileTypes', v: 'video/mp4' },
- { op: '=', k: 'attachedFileTypes', v: 'video/webm' },
- { op: '=', k: 'attachedFileTypes', v: 'video/mpeg' },
- { op: '=', k: 'attachedFileTypes', v: 'video/x-m4v' },
- ] });
- } else if (opts.filetype === 'audio') {
- filter.qs.push({ op: 'or', qs: [
- { op: '=', k: 'attachedFileTypes', v: 'audio/mpeg' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/flac' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/wav' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/aac' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/webm' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/opus' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/ogg' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/x-m4a' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/mod' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/s3m' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/xm' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/it' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/x-mod' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/x-s3m' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/x-xm' },
- { op: '=', k: 'attachedFileTypes', v: 'audio/x-it' },
- ] });
- }
+ case 'meilisearch': {
+ return this.searchNoteByMeiliSearch(q, me, opts, pagination);
}
- const res = await this.meilisearchNoteIndex!.search(q, {
- sort: [`createdAt:${opts.order ? opts.order : 'desc'}`],
- matchingStrategy: 'all',
- attributesToRetrieve: ['id', 'createdAt'],
- filter: compileQuery(filter),
- limit: pagination.limit,
- });
- if (res.hits.length === 0) return [];
- const [
- userIdsWhoMeMuting,
- userIdsWhoBlockingMe,
- ] = me ? await Promise.all([
- this.cacheService.userMutingsCache.fetch(me.id),
- this.cacheService.userBlockedCache.fetch(me.id),
- ]) : [new Set<string>(), new Set<string>()];
- const notes = (await this.notesRepository.findBy({
- id: In(res.hits.map(x => x.id)),
- })).filter(note => {
- if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
- if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
- return true;
- });
- return notes.sort((a, b) => a.id > b.id ? -1 : 1);
+ default: {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const typeCheck: never = this.provider;
+ return [];
+ }
+ }
+ }
+
+ @bindThis
+ private async searchNoteByLike(
+ q: string,
+ me: MiUser | null,
+ opts: SearchOpts,
+ pagination: SearchPagination,
+ ): Promise<MiNote[]> {
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId);
+
+ if (opts.userId) {
+ query.andWhere('note.userId = :userId', { userId: opts.userId });
+ } else if (opts.channelId) {
+ query.andWhere('note.channelId = :channelId', { channelId: opts.channelId });
+ }
+
+ query
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
+
+ if (this.config.fulltextSearch?.provider === 'sqlPgroonga') {
+ query.andWhere('note.text &@~ :q', { q });
+ } else if (this.config.fulltextSearch?.provider === 'sqlTsvector') {
+ query.andWhere('note.tsvector_embedding @@ websearch_to_tsquery(:q)', { q });
} else {
- const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId);
+ query.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` });
+ }
- if (opts.userId) {
- query.andWhere('note.userId = :userId', { userId: opts.userId });
- } else if (opts.channelId) {
- query.andWhere('note.channelId = :channelId', { channelId: opts.channelId });
+ if (opts.host) {
+ if (opts.host === '.') {
+ query.andWhere('note.userHost IS NULL');
+ } else {
+ query.andWhere('note.userHost = :host', { host: opts.host });
}
+ }
- query
- .andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('renote.user', 'renoteUser');
+ if (opts.filetype) {
+ query.andWhere('note."attachedFileTypes" && :types', { types: fileTypes[opts.filetype] });
+ }
- if (opts.host) {
- if (opts.host === '.') {
- query.andWhere('user.host IS NULL');
- } else {
- query.andWhere('user.host = :host', { host: opts.host });
- }
- }
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) this.queryService.generateMutedUserQuery(query, me);
+ if (me) this.queryService.generateBlockedUserQuery(query, me);
+
+ return await query.limit(pagination.limit).getMany();
+ }
- if (opts.filetype) {
- /* this is very ugly, but the "correct" solution would
- be `and exists (select 1 from
- unnest(note."attachedFileTypes") x(t) where t like
- :type)` and I can't find a way to get TypeORM to
- generate that; this hack works because `~*` is
- "regexp match, ignoring case" and the stringified
- version of an array of varchars (which is what
- `attachedFileTypes` is) looks like `{foo,bar}`, so
- we're looking for opts.filetype as the first half of
- a MIME type, either at start of the array (after the
- `{`) or later (after a `,`) */
- query.andWhere(`note."attachedFileTypes"::varchar ~* :type`, { type: `[{,]${opts.filetype}/` });
+ @bindThis
+ private async searchNoteByMeiliSearch(
+ q: string,
+ me: MiUser | null,
+ opts: SearchOpts,
+ pagination: SearchPagination,
+ ): Promise<MiNote[]> {
+ if (!this.meilisearch || !this.meilisearchNoteIndex) {
+ throw new Error('MeiliSearch is not available');
+ }
+
+ const filter: Q = {
+ op: 'and',
+ qs: [],
+ };
+ if (pagination.untilId) filter.qs.push({
+ op: '<',
+ k: 'createdAt',
+ v: this.idService.parse(pagination.untilId).date.getTime(),
+ });
+ if (pagination.sinceId) filter.qs.push({
+ op: '>',
+ k: 'createdAt',
+ v: this.idService.parse(pagination.sinceId).date.getTime(),
+ });
+ if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
+ if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
+ if (opts.host) {
+ if (opts.host === '.') {
+ filter.qs.push({ op: 'is null', k: 'userHost' });
+ } else {
+ filter.qs.push({ op: '=', k: 'userHost', v: opts.host });
}
+ }
- this.queryService.generateVisibilityQuery(query, me);
- if (me) this.queryService.generateMutedUserQuery(query, me);
- if (me) this.queryService.generateBlockedUserQuery(query, me);
+ if (opts.filetype) {
+ const filters = fileTypes[opts.filetype].map(mime => ({ op: '=' as const, k: 'attachedFileTypes', v: mime }));
+ filter.qs.push({ op: 'or', qs: filters });
+ }
- return await query.limit(pagination.limit).getMany();
+ const res = await this.meilisearchNoteIndex.search(q, {
+ sort: [`createdAt:${opts.order ? opts.order : 'desc'}`],
+ matchingStrategy: 'all',
+ attributesToRetrieve: ['id', 'createdAt'],
+ filter: compileQuery(filter),
+ limit: pagination.limit,
+ });
+ if (res.hits.length === 0) {
+ return [];
}
+
+ const [
+ userIdsWhoMeMuting,
+ userIdsWhoBlockingMe,
+ ] = me
+ ? await Promise.all([
+ this.cacheService.userMutingsCache.fetch(me.id),
+ this.cacheService.userBlockedCache.fetch(me.id),
+ ])
+ : [new Set<string>(), new Set<string>()];
+ const notes = (await this.notesRepository.findBy({
+ id: In(res.hits.map(x => x.id)),
+ })).filter(note => {
+ if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
+ if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
+ return true;
+ });
+
+ return notes.sort((a, b) => a.id > b.id ? -1 : 1);
}
}
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 0ad448e95f..9fc0c2b34a 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -162,3 +162,4 @@ export class SignupService {
return { account, secret };
}
}
+
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
index de00169612..8239490adc 100644
--- a/packages/backend/src/core/SystemWebhookService.ts
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -50,7 +50,6 @@ export type SystemWebhookPayload<T extends SystemWebhookEventType> =
@Injectable()
export class SystemWebhookService implements OnApplicationShutdown {
- private logger: Logger;
private activeSystemWebhooksFetched = false;
private activeSystemWebhooks: MiSystemWebhook[] = [];
@@ -62,11 +61,9 @@ export class SystemWebhookService implements OnApplicationShutdown {
private idService: IdService,
private queueService: QueueService,
private moderationLogService: ModerationLogService,
- private loggerService: LoggerService,
private globalEventService: GlobalEventService,
) {
this.redisForSub.on('message', this.onMessage);
- this.logger = this.loggerService.getLogger('webhook');
}
@bindThis
@@ -193,28 +190,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
/**
* SystemWebhook をWebhook配送キューに追加する
* @see QueueService.systemWebhookDeliver
- * // TODO: contentの型を厳格化する
*/
@bindThis
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
- webhook: MiSystemWebhook | MiSystemWebhook['id'],
type: T,
content: SystemWebhookPayload<T>,
+ opts?: {
+ excludes?: MiSystemWebhook['id'][];
+ },
) {
- const webhookEntity = typeof webhook === 'string'
- ? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
- : webhook;
- if (!webhookEntity || !webhookEntity.isActive) {
- this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
- return;
- }
-
- if (!webhookEntity.on.includes(type)) {
- this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
- return;
- }
-
- return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
+ const webhooks = await this.fetchActiveSystemWebhooks()
+ .then(webhooks => {
+ return webhooks.filter(webhook => !opts?.excludes?.includes(webhook.id) && webhook.on.includes(type));
+ });
+ return Promise.all(
+ webhooks.map(webhook => {
+ return this.queueService.systemWebhookDeliver(webhook, type, content);
+ }),
+ );
}
@bindThis
diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 2f1310b8ef..8da1bb2092 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -118,13 +118,7 @@ export class UserBlockingService implements OnModuleInit {
schema: 'UserDetailedNotMe',
}).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'unfollow', {
- user: packed,
- });
- }
+ this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packed });
});
}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 8963003057..b98ca97ec9 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -333,13 +333,7 @@ export class UserFollowingService implements OnModuleInit {
schema: 'UserDetailedNotMe',
}).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'follow', packed);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'follow', {
- user: packed,
- });
- }
+ this.webhookService.enqueueUserWebhook(follower.id, 'follow', { user: packed });
});
}
@@ -347,13 +341,7 @@ export class UserFollowingService implements OnModuleInit {
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(async packed => {
this.globalEventService.publishMainStream(followee.id, 'followed', packed);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'followed', {
- user: packed,
- });
- }
+ this.webhookService.enqueueUserWebhook(followee.id, 'followed', { user: packed });
});
// 通知を作成
@@ -400,13 +388,7 @@ export class UserFollowingService implements OnModuleInit {
schema: 'UserDetailedNotMe',
}).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'unfollow', {
- user: packed,
- });
- }
+ this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packed });
});
}
@@ -744,13 +726,7 @@ export class UserFollowingService implements OnModuleInit {
});
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
-
- const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
- for (const webhook of webhooks) {
- this.queueService.userWebhookDeliver(webhook, 'unfollow', {
- user: packedFollowee,
- });
- }
+ this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packedFollowee });
}
@bindThis
diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts
index 6333356fe9..4f4d59a02c 100644
--- a/packages/backend/src/core/UserListService.ts
+++ b/packages/backend/src/core/UserListService.ts
@@ -58,7 +58,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
}
async onModuleInit() {
- this.roleService = this.moduleRef.get(RoleService.name);
+ this.roleService = this.moduleRef.get('RoleService');
}
@bindThis
diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts
index 9b1961c631..1f471513f3 100644
--- a/packages/backend/src/core/UserService.ts
+++ b/packages/backend/src/core/UserService.ts
@@ -63,13 +63,6 @@ export class UserService {
@bindThis
public async notifySystemWebhook(user: MiUser, type: 'userCreated') {
const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' });
- const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] });
- for (const webhookId of recipientWebhookIds) {
- await this.systemWebhookService.enqueueSystemWebhook(
- webhookId,
- type,
- packedUser,
- );
- }
+ return this.systemWebhookService.enqueueSystemWebhook(type, packedUser);
}
}
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
index 911efdf768..08db4c9afc 100644
--- a/packages/backend/src/core/UserWebhookService.ts
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -5,13 +5,14 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import { type WebhooksRepository } from '@/models/_.js';
+import { MiUser, type WebhooksRepository } from '@/models/_.js';
import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
+import { QueueService } from '@/core/QueueService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
export type UserWebhookPayload<T extends WebhookEventTypes> =
T extends 'note' | 'reply' | 'renote' |'mention' | 'edited' ? {
@@ -34,6 +35,7 @@ export class UserWebhookService implements OnApplicationShutdown {
private redisForSub: Redis.Redis,
@Inject(DI.webhooksRepository)
private webhooksRepository: WebhooksRepository,
+ private queueService: QueueService,
) {
this.redisForSub.on('message', this.onMessage);
}
@@ -75,6 +77,25 @@ export class UserWebhookService implements OnApplicationShutdown {
return query.getMany();
}
+ /**
+ * UserWebhook をWebhook配送キューに追加する
+ * @see QueueService.userWebhookDeliver
+ */
+ @bindThis
+ public async enqueueUserWebhook<T extends WebhookEventTypes>(
+ userId: MiUser['id'],
+ type: T,
+ content: UserWebhookPayload<T>,
+ ) {
+ const webhooks = await this.getActiveWebhooks()
+ .then(webhooks => webhooks.filter(webhook => webhook.userId === userId && webhook.on.includes(type)));
+ return Promise.all(
+ webhooks.map(webhook => {
+ return this.queueService.userWebhookDeliver(webhook, type, content);
+ }),
+ );
+ }
+
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index f905914022..81eaa5f95d 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -3,8 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { URL } from 'node:url';
-import punycode from 'punycode/punycode.js';
+import { URL, domainToASCII } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import RE2 from 're2';
import psl from 'psl';
@@ -107,13 +106,13 @@ export class UtilityService {
@bindThis
public toPuny(host: string): string {
- return punycode.toASCII(host.toLowerCase());
+ return domainToASCII(host.toLowerCase());
}
@bindThis
public toPunyNullable(host: string | null | undefined): string | null {
if (host == null) return null;
- return punycode.toASCII(host.toLowerCase());
+ return domainToASCII(host.toLowerCase());
}
@bindThis
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index ad53192f18..ed75e4f467 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -189,14 +189,12 @@ export class WebAuthnService {
*/
@bindThis
public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> {
- const challenge = await this.redisClient.get(`webauthn:challenge:${context}`);
+ const challenge = await this.redisClient.getdel(`webauthn:challenge:${context}`);
if (!challenge) {
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`);
}
- await this.redisClient.del(`webauthn:challenge:${context}`);
-
const key = await this.userSecurityKeysRepository.findOneBy({
id: response.id,
});
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index dfe7a259c4..2e50f4472f 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -99,6 +99,8 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
signupReason: null,
noindex: false,
enableRss: true,
+ mandatoryCW: null,
+ rejectQuotes: false,
...override,
};
}
@@ -142,6 +144,7 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
renoteUserId: null,
renoteUserHost: null,
updatedAt: null,
+ processErrors: [],
...override,
};
}
@@ -216,6 +219,7 @@ function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'
isSystem: false,
isSilenced: user.isSilenced,
enableRss: true,
+ mandatoryCW: null,
...override,
};
}
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 278c97f907..1eef85aeef 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -363,10 +363,12 @@ export class ApInboxService {
this.logger.info(`Creating the (Re)Note: ${uri}`);
const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc, resolver);
- const createdAt = activity.published ? new Date(activity.published) : null;
+ let createdAt = activity.published ? new Date(activity.published) : null;
- if (createdAt && createdAt < this.idService.parse(renote.id).date) {
- return 'skip: malformed createdAt';
+ const renoteDate = this.idService.parse(renote.id).date;
+ if (createdAt && createdAt < renoteDate) {
+ this.logger.warn(`Correcting invalid publish time for Announce "${uri}"`);
+ createdAt = renoteDate;
}
await this.noteCreateService.create(actor, {
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index fb706a775f..cb9b74f6d7 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -28,6 +28,7 @@ import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFil
import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { IdService } from '@/core/IdService.js';
+import { appendContentWarning } from '@/misc/append-content-warning.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js';
@@ -192,6 +193,9 @@ export class ApRendererService {
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
},
+ _misskey_license: {
+ freeText: emoji.license,
+ },
};
}
@@ -336,7 +340,7 @@ export class ApRendererService {
}
@bindThis
- public async renderNote(note: MiNote, dive = true): Promise<IPost> {
+ public async renderNote(note: MiNote, author: MiUser, dive = true): Promise<IPost> {
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
@@ -350,14 +354,14 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
- const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
+ const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
- if (inReplyToUserExist) {
+ if (inReplyToUser) {
if (inReplyToNote.uri) {
inReplyTo = inReplyToNote.uri;
} else {
if (dive) {
- inReplyTo = await this.renderNote(inReplyToNote, false);
+ inReplyTo = await this.renderNote(inReplyToNote, inReplyToUser, false);
} else {
inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`;
}
@@ -420,7 +424,12 @@ export class ApRendererService {
apAppend += `\n\nRE: ${quote}`;
}
- const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
+ let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
+
+ // Apply mandatory CW, if applicable
+ if (author.mandatoryCW) {
+ summary = appendContentWarning(summary, author.mandatoryCW);
+ }
const { content } = this.apMfmService.getNoteHtml(note, apAppend);
@@ -633,7 +642,7 @@ export class ApRendererService {
}
@bindThis
- public async renderUpNote(note: MiNote, dive = true): Promise<IPost> {
+ public async renderUpNote(note: MiNote, author: MiUser, dive = true): Promise<IPost> {
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
@@ -647,14 +656,14 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
- const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
+ const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
- if (inReplyToUserExist) {
+ if (inReplyToUser) {
if (inReplyToNote.uri) {
inReplyTo = inReplyToNote.uri;
} else {
if (dive) {
- inReplyTo = await this.renderUpNote(inReplyToNote, false);
+ inReplyTo = await this.renderUpNote(inReplyToNote, inReplyToUser, false);
} else {
inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`;
}
@@ -717,7 +726,12 @@ export class ApRendererService {
apAppend += `\n\nRE: ${quote}`;
}
- const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
+ let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
+
+ // Apply mandatory CW, if applicable
+ if (author.mandatoryCW) {
+ summary = appendContentWarning(summary, author.mandatoryCW);
+ }
const { content } = this.apMfmService.getNoteHtml(note, apAppend);
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 8036c9638f..b63d4eb2ab 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -11,13 +11,12 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
-import { UtilityService } from '@/core/UtilityService.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
-import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from './type.js';
type Request = {
@@ -148,7 +147,7 @@ export class ApRequestService {
private userKeypairService: UserKeypairService,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
- private utilityService: UtilityService,
+ private readonly apUtilityService: ApUtilityService,
) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
@@ -183,9 +182,10 @@ export class ApRequestService {
* Get AP object with http-signature
* @param user http-signature user
* @param url URL to fetch
+ * @param followAlternate
*/
@bindThis
- public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
+ public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<IObject> {
const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id);
@@ -239,13 +239,22 @@ export class ApRequestService {
try {
document.documentElement.innerHTML = html;
- const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
+ // Search for any matching value in priority order:
+ // 1. Type=AP > Type=none > Type=anything
+ // 2. Alternate > Canonical
+ // 3. Page order (fallback)
+ const alternate =
+ document.querySelector('head > link[href][rel="alternate"][type="application/activity+json"]') ??
+ document.querySelector('head > link[href][rel="canonical"][type="application/activity+json"]') ??
+ document.querySelector('head > link[href][rel="alternate"]:not([type])') ??
+ document.querySelector('head > link[href][rel="canonical"]:not([type])') ??
+ document.querySelector('head > link[href][rel="alternate"]') ??
+ document.querySelector('head > link[href][rel="canonical"]');
+
if (alternate) {
const href = alternate.getAttribute('href');
- if (href) {
- if (this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) {
- return await this.signedGet(href, user, false);
- }
+ if (href && this.apUtilityService.haveSameAuthority(url, href)) {
+ return await this.signedGet(href, user, false);
}
}
} catch (e) {
@@ -258,10 +267,11 @@ export class ApRequestService {
validateContentTypeSetAsActivityPub(res);
- const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
- assertActivityMatchesUrls(activity, [finalUrl]);
+ // Make sure the object ID matches the final URL (which is where it actually exists).
+ // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
+ this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
return activity;
}
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index c82a9be3b1..f9ccf10fa7 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -5,10 +5,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
-import { UnrecoverableError } from 'bullmq';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
-import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
+import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta, SkApFetchLog } from '@/models/_.js';
import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { DI } from '@/di-symbols.js';
@@ -17,7 +16,10 @@ import { bindThis } from '@/decorators.js';
import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { fromTuple } from '@/misc/from-tuple.js';
-import { isCollectionOrOrderedCollection } from './type.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { ApLogService, calculateDurationSince, extractObjectContext } from '@/core/ApLogService.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
+import { getApId, getNullableApId, isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js';
@@ -43,6 +45,8 @@ export class Resolver {
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private loggerService: LoggerService,
+ private readonly apLogService: ApLogService,
+ private readonly apUtilityService: ApUtilityService,
private recursionLimit = 256,
) {
this.history = new Set();
@@ -68,7 +72,7 @@ export class Resolver {
if (isCollectionOrOrderedCollection(collection)) {
return collection;
} else {
- throw new UnrecoverableError(`unrecognized collection type: ${collection.type}`);
+ throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `unrecognized collection type: ${collection.type}`);
}
}
@@ -81,30 +85,67 @@ export class Resolver {
return value;
}
+ const host = this.utilityService.extractDbHost(value);
+ if (this.config.activityLogging.enabled && !this.utilityService.isSelfHost(host)) {
+ return await this._resolveLogged(value, host);
+ } else {
+ return await this._resolve(value, host);
+ }
+ }
+
+ private async _resolveLogged(requestUri: string, host: string): Promise<IObject> {
+ const startTime = process.hrtime.bigint();
+
+ const log = await this.apLogService.createFetchLog({
+ host: host,
+ requestUri,
+ });
+
+ try {
+ const result = await this._resolve(requestUri, host, log);
+
+ log.accepted = true;
+ log.result = 'ok';
+
+ return result;
+ } catch (err) {
+ log.accepted = false;
+ log.result = String(err);
+
+ throw err;
+ } finally {
+ log.duration = calculateDurationSince(startTime);
+
+ // Save or finalize asynchronously
+ this.apLogService.saveFetchLog(log)
+ .catch(err => this.logger.error('Failed to record AP object fetch:', err));
+ }
+ }
+
+ private async _resolve(value: string, host: string, log?: SkApFetchLog): Promise<IObject> {
if (value.includes('#')) {
// URLs with fragment parts cannot be resolved correctly because
// the fragment part does not get transmitted over HTTP(S).
// Avoid strange behaviour by not trying to resolve these at all.
- throw new UnrecoverableError(`cannot resolve URL with fragment: ${value}`);
+ throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `cannot resolve URL with fragment: ${value}`);
}
if (this.history.has(value)) {
- throw new Error(`cannot resolve already resolved URL: ${value}`);
+ throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', `cannot resolve already resolved URL: ${value}`);
}
if (this.history.size > this.recursionLimit) {
- throw new Error(`hit recursion limit: ${value}`);
+ throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `hit recursion limit: ${value}`);
}
this.history.add(value);
- const host = this.utilityService.extractDbHost(value);
if (this.utilityService.isSelfHost(host)) {
return await this.resolveLocal(value);
}
if (!this.utilityService.isFederationAllowedHost(host)) {
- throw new UnrecoverableError(`cannot fetch AP object ${value}: blocked instance ${host}`);
+ throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', `cannot fetch AP object ${value}: blocked instance ${host}`);
}
if (this.config.signToActivityPubGet && !this.user) {
@@ -115,32 +156,42 @@ export class Resolver {
? await this.apRequestService.signedGet(value, this.user) as IObject
: await this.httpRequestService.getActivityJson(value)) as IObject;
+ if (log) {
+ const { object: objectOnly, context, contextHash } = extractObjectContext(object);
+ const objectUri = getNullableApId(object);
+
+ if (objectUri) {
+ log.objectUri = objectUri;
+ log.host = this.utilityService.extractDbHost(objectUri);
+ }
+
+ log.object = objectOnly;
+ log.context = context;
+ log.contextHash = contextHash;
+ }
+
if (
Array.isArray(object['@context']) ?
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
) {
- throw new UnrecoverableError(`invalid AP object ${value}: does not have ActivityStreams context`);
+ throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', `invalid AP object ${value}: does not have ActivityStreams context`);
}
- // Since redirects are allowed, we cannot safely validate an anonymous object.
- // Reject any responses without an ID, as all other checks depend on that value.
- if (object.id == null) {
- throw new UnrecoverableError(`invalid AP object ${value}: missing id`);
- }
+ // The object ID is already validated to match the final URL's authority by signedGet / getActivityJson.
+ // We only need to validate that it also matches the original URL's authority, in case of redirects.
+ const objectId = getApId(object);
// We allow some limited cross-domain redirects, which means the host may have changed during fetch.
// Additional checks are needed to validate the scope of cross-domain redirects.
- const finalHost = this.utilityService.extractDbHost(object.id);
+ const finalHost = this.utilityService.extractDbHost(objectId);
if (finalHost !== host) {
// Make sure the redirect stayed within the same authority.
- if (this.utilityService.punyHostPSLDomain(object.id) !== this.utilityService.punyHostPSLDomain(value)) {
- throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`);
- }
+ this.apUtilityService.assertIdMatchesUrlAuthority(object, value);
// Check if the redirect bounce from [allowed domain] to [blocked domain].
if (!this.utilityService.isFederationAllowedHost(finalHost)) {
- throw new UnrecoverableError(`cannot fetch AP object ${value}: redirected to blocked instance ${finalHost}`);
+ throw new IdentifiableError('0a72bf24-2d9b-4f1d-886b-15aaa31adeda', `cannot fetch AP object ${value}: redirected to blocked instance ${finalHost}`);
}
}
@@ -150,17 +201,18 @@ export class Resolver {
@bindThis
private resolveLocal(url: string): Promise<IObject> {
const parsed = this.apDbResolverService.parseUri(url);
- if (!parsed.local) throw new UnrecoverableError(`resolveLocal - not a local URL: ${url}`);
+ if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${url}`);
switch (parsed.type) {
case 'notes':
return this.notesRepository.findOneByOrFail({ id: parsed.id })
.then(async note => {
+ const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
if (parsed.rest === 'activity') {
// this refers to the create activity and not the note itself
- return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note), note));
+ return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author), note));
} else {
- return this.apRendererService.renderNote(note);
+ return this.apRendererService.renderNote(note, author);
}
});
case 'users':
@@ -179,7 +231,7 @@ export class Resolver {
case 'follows':
return this.followRequestsRepository.findOneBy({ id: parsed.id })
.then(async followRequest => {
- if (followRequest == null) throw new UnrecoverableError(`resolveLocal - invalid follow request ID ${parsed.id}: ${url}`);
+ if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', `resolveLocal - invalid follow request ID ${parsed.id}: ${url}`);
const [follower, followee] = await Promise.all([
this.usersRepository.findOneBy({
id: followRequest.followerId,
@@ -191,12 +243,12 @@ export class Resolver {
}),
]);
if (follower == null || followee == null) {
- throw new Error(`resolveLocal - follower or followee does not exist: ${url}`);
+ throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', `resolveLocal - follower or followee does not exist: ${url}`);
}
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
});
default:
- throw new UnrecoverableError(`resolveLocal: type ${parsed.type} unhandled: ${url}`);
+ throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `resolveLocal: type ${parsed.type} unhandled: ${url}`);
}
}
}
@@ -232,6 +284,8 @@ export class ApResolverService {
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private loggerService: LoggerService,
+ private readonly apLogService: ApLogService,
+ private readonly apUtilityService: ApUtilityService,
) {
}
@@ -252,6 +306,8 @@ export class ApResolverService {
this.apRendererService,
this.apDbResolverService,
this.loggerService,
+ this.apLogService,
+ this.apUtilityService,
);
}
}
diff --git a/packages/backend/src/core/activitypub/ApUtilityService.ts b/packages/backend/src/core/activitypub/ApUtilityService.ts
new file mode 100644
index 0000000000..ae6e4997e4
--- /dev/null
+++ b/packages/backend/src/core/activitypub/ApUtilityService.ts
@@ -0,0 +1,108 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { UtilityService } from '@/core/UtilityService.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { toArray } from '@/misc/prelude/array.js';
+import { EnvService } from '@/core/EnvService.js';
+import { getApId, getOneApHrefNullable, IObject } from './type.js';
+
+@Injectable()
+export class ApUtilityService {
+ constructor(
+ private readonly utilityService: UtilityService,
+ private readonly envService: EnvService,
+ ) {}
+
+ /**
+ * Verifies that the object's ID has the same authority as the provided URL.
+ * Returns on success, throws on any validation error.
+ */
+ public assertIdMatchesUrlAuthority(object: IObject, url: string): void {
+ // This throws if the ID is missing or invalid, but that's ok.
+ // Anonymous objects are impossible to verify, so we don't allow fetching them.
+ const id = getApId(object);
+
+ // Make sure the object ID matches the final URL (which is where it actually exists).
+ // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
+ if (!this.haveSameAuthority(url, id)) {
+ throw new IdentifiableError('fd93c2fa-69a8-440f-880b-bf178e0ec877', `invalid AP object ${url}: id ${id} has different host authority`);
+ }
+ }
+
+ /**
+ * Checks if two URLs have the same host authority
+ */
+ public haveSameAuthority(url1: string, url2: string): boolean {
+ if (url1 === url2) return true;
+
+ const authority1 = this.utilityService.punyHostPSLDomain(url1);
+ const authority2 = this.utilityService.punyHostPSLDomain(url2);
+ return authority1 === authority2;
+ }
+
+ /**
+ * Finds the "best" URL for a given AP object.
+ * The list of URLs is first filtered via findSameAuthorityUrl, then further filtered based on mediaType, and finally sorted to select the best one.
+ * @throws {IdentifiableError} if object does not have an ID
+ * @returns the best URL, or null if none were found
+ */
+ public findBestObjectUrl(object: IObject): string | null {
+ const targetUrl = getApId(object);
+ const targetAuthority = this.utilityService.punyHostPSLDomain(targetUrl);
+
+ const rawUrls = toArray(object.url);
+ const acceptableUrls = rawUrls
+ .map(raw => ({
+ url: getOneApHrefNullable(raw),
+ type: typeof(raw) === 'object'
+ ? raw.mediaType?.toLowerCase()
+ : undefined,
+ }))
+ .filter(({ url, type }) => {
+ if (!url) return false;
+ if (!this.checkHttps(url)) return false;
+ if (!isAcceptableUrlType(type)) return false;
+
+ const urlAuthority = this.utilityService.punyHostPSLDomain(url);
+ return urlAuthority === targetAuthority;
+ })
+ .sort((a, b) => {
+ return rankUrlType(a.type) - rankUrlType(b.type);
+ });
+
+ return acceptableUrls[0]?.url ?? null;
+ }
+
+ /**
+ * Checks if the URL contains HTTPS.
+ * Additionally, allows HTTP in non-production environments.
+ * Based on check-https.ts.
+ */
+ private checkHttps(url: string): boolean {
+ const isNonProd = this.envService.env.NODE_ENV !== 'production';
+
+ // noinspection HttpUrlsUsage
+ return url.startsWith('https://') || (url.startsWith('http://') && isNonProd);
+ }
+}
+
+function isAcceptableUrlType(type: string | undefined): boolean {
+ if (!type) return true;
+ if (type.startsWith('text/')) return true;
+ if (type.startsWith('application/ld+json')) return true;
+ if (type.startsWith('application/activity+json')) return true;
+ return false;
+}
+
+function rankUrlType(type: string | undefined): number {
+ if (!type) return 2;
+ if (type === 'text/html') return 0;
+ if (type.startsWith('text/')) return 1;
+ if (type.startsWith('application/ld+json')) return 3;
+ if (type.startsWith('application/activity+json')) return 4;
+ return 5;
+}
diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts
deleted file mode 100644
index edfab5a216..0000000000
--- a/packages/backend/src/core/activitypub/misc/check-against-url.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SPDX-FileCopyrightText: dakkar and sharkey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { UnrecoverableError } from 'bullmq';
-import type { IObject } from '../type.js';
-
-function getHrefsFrom(one: IObject | string | undefined | (IObject | string | undefined)[]): (string | undefined)[] {
- if (Array.isArray(one)) {
- return one.flatMap(h => getHrefsFrom(h));
- }
- return [
- typeof(one) === 'object' ? one.href : one,
- ];
-}
-
-export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
- const expectedUrls = new Set(urls
- .filter(u => URL.canParse(u))
- .map(u => new URL(u).href),
- );
-
- const actualUrls = [activity.id, ...getHrefsFrom(activity.url)]
- .filter(u => u && URL.canParse(u))
- .map(u => new URL(u as string).href);
-
- if (!actualUrls.some(u => expectedUrls.has(u))) {
- throw new UnrecoverableError(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`);
- }
-}
diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts
index d7b6fc6589..5c0b8ffcbb 100644
--- a/packages/backend/src/core/activitypub/misc/contexts.ts
+++ b/packages/backend/src/core/activitypub/misc/contexts.ts
@@ -561,6 +561,11 @@ const extension_context_definition = {
'_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
'_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore',
'_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore',
+ '_misskey_license': 'misskey:_misskey_license',
+ 'freeText': {
+ '@id': 'misskey:freeText',
+ '@type': 'schema:text',
+ },
'isCat': 'misskey:isCat',
// Firefish
firefish: 'https://joinfirefish.org/ns#',
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index e4c4fe54b5..63f9887a8d 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -25,12 +25,14 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
-import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
+import { isRetryableError } from '@/misc/is-retryable-error.js';
+import { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApAudienceService } from '../ApAudienceService.js';
+import { ApUtilityService } from '../ApUtilityService.js';
import { ApPersonService } from './ApPersonService.js';
import { extractApHashtags } from './tag.js';
import { ApMentionService } from './ApMentionService.js';
@@ -81,6 +83,7 @@ export class ApNoteService {
private noteEditService: NoteEditService,
private apDbResolverService: ApDbResolverService,
private apLoggerService: ApLoggerService,
+ private readonly apUtilityService: ApUtilityService,
) {
this.logger = this.apLoggerService.logger;
}
@@ -91,7 +94,6 @@ export class ApNoteService {
uri: string,
actor?: MiRemoteUser,
user?: MiRemoteUser,
- note?: MiNote,
): Error | null {
const expectHost = this.utilityService.extractDbHost(uri);
const apType = getApType(object);
@@ -123,13 +125,6 @@ export class ApNoteService {
}
}
- if (note) {
- const url = (object.url) ? getOneApId(object.url) : note.url;
- if (url && url !== note.url) {
- return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated url does not match original url. updated url: ${url}, original url: ${note.url}`);
- }
- }
-
return null;
}
@@ -185,17 +180,7 @@ export class ApNoteService {
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`);
}
- const url = getOneApHrefNullable(note.url);
-
- if (url != null) {
- if (!checkHttps(url)) {
- throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`);
- }
-
- if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) {
- throw new UnrecoverableError(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`);
- }
- }
+ const url = this.apUtilityService.findBestObjectUrl(note);
this.logger.info(`Creating the Note: ${note.id}`);
@@ -270,6 +255,14 @@ export class ApNoteService {
if (file) files.push(file);
}
+ // Some software (Peertube) attaches a thumbnail under "icon" instead of "attachment"
+ const icon = getBestIcon(note);
+ if (icon) {
+ icon.sensitive ??= note.sensitive;
+ const file = await this.apImageService.resolveImage(actor, icon);
+ if (file) files.push(file);
+ }
+
// リプライ
const reply: MiNote | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, { resolver })
@@ -288,44 +281,8 @@ export class ApNoteService {
: null;
// 引用
- let quote: MiNote | undefined | null = null;
-
- if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
- const tryResolveNote = async (uri: unknown): Promise<
- | { status: 'ok'; res: MiNote }
- | { status: 'permerror' | 'temperror' }
- > => {
- if (typeof uri !== 'string' || !/^https?:/.test(uri)) {
- this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`);
- return { status: 'permerror' };
- }
- try {
- const res = await this.resolveNote(uri, { resolver });
- if (res == null) {
- this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`);
- return { status: 'permerror' };
- }
- return { status: 'ok', res };
- } catch (e) {
- const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
- this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`);
-
- return {
- status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
- };
- }
- };
-
- const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null));
- const results = await Promise.all(uris.map(tryResolveNote));
-
- quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
- if (!quote) {
- if (results.some(x => x.status === 'temperror')) {
- throw new Error(`temporary error resolving quote for ${entryUri}`);
- }
- }
- }
+ const quote = await this.getQuote(note, entryUri, resolver);
+ const processErrors = quote === null ? ['quoteUnavailable'] : null;
// vote
if (reply && reply.hasPoll) {
@@ -361,7 +318,8 @@ export class ApNoteService {
createdAt: note.published ? new Date(note.published) : null,
files,
reply,
- renote: quote,
+ renote: quote ?? null,
+ processErrors,
name: note.name,
cw,
text,
@@ -411,7 +369,7 @@ export class ApNoteService {
const object = await resolver.resolve(value);
const entryUri = getApId(value);
- const err = this.validateNote(object, entryUri, actor, user, updatedNote);
+ const err = this.validateNote(object, entryUri, actor, user);
if (err) {
this.logger.error(err.message, {
resolver: { history: resolver.getHistory() },
@@ -437,17 +395,7 @@ export class ApNoteService {
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`);
}
- const url = getOneApHrefNullable(note.url);
-
- if (url != null) {
- if (!checkHttps(url)) {
- throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`);
- }
-
- if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) {
- throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`);
- }
- }
+ const url = this.apUtilityService.findBestObjectUrl(note);
this.logger.info(`Creating the Note: ${note.id}`);
@@ -504,6 +452,14 @@ export class ApNoteService {
if (file) files.push(file);
}
+ // Some software (Peertube) attaches a thumbnail under "icon" instead of "attachment"
+ const icon = getBestIcon(note);
+ if (icon) {
+ icon.sensitive ??= note.sensitive;
+ const file = await this.apImageService.resolveImage(actor, icon);
+ if (file) files.push(file);
+ }
+
// リプライ
const reply: MiNote | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, { resolver })
@@ -522,44 +478,8 @@ export class ApNoteService {
: null;
// 引用
- let quote: MiNote | undefined | null = null;
-
- if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
- const tryResolveNote = async (uri: unknown): Promise<
- | { status: 'ok'; res: MiNote }
- | { status: 'permerror' | 'temperror' }
- > => {
- if (typeof uri !== 'string' || !/^https?:/.test(uri)) {
- this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`);
- return { status: 'permerror' };
- }
- try {
- const res = await this.resolveNote(uri, { resolver });
- if (res == null) {
- this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`);
- return { status: 'permerror' };
- }
- return { status: 'ok', res };
- } catch (e) {
- const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
- this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`);
-
- return {
- status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
- };
- }
- };
-
- const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null));
- const results = await Promise.all(uris.map(tryResolveNote));
-
- quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
- if (!quote) {
- if (results.some(x => x.status === 'temperror')) {
- throw new Error(`temporary error resolving quote for ${entryUri}`);
- }
- }
- }
+ const quote = await this.getQuote(note, entryUri, resolver);
+ const processErrors = quote === null ? ['quoteUnavailable'] : null;
// vote
if (reply && reply.hasPoll) {
@@ -595,7 +515,8 @@ export class ApNoteService {
createdAt: note.published ? new Date(note.published) : null,
files,
reply,
- renote: quote,
+ renote: quote ?? null,
+ processErrors,
name: note.name,
cw,
text,
@@ -690,6 +611,8 @@ export class ApNoteService {
originalUrl: tag.icon.url,
publicUrl: tag.icon.url,
updatedAt: new Date(),
+ // _misskey_license が存在しなければ `null`
+ license: (tag._misskey_license?.freeText ?? null),
});
const emoji = await this.emojisRepository.findOneBy({ host, name });
@@ -711,7 +634,87 @@ export class ApNoteService {
publicUrl: tag.icon.url,
updatedAt: new Date(),
aliases: [],
+ // _misskey_license が存在しなければ `null`
+ license: (tag._misskey_license?.freeText ?? null)
});
}));
}
+
+ /**
+ * Fetches the note's quoted post.
+ * On success - returns the note.
+ * On skip (no quote) - returns undefined.
+ * On permanent error - returns null.
+ * On temporary error - throws an exception.
+ */
+ private async getQuote(note: IPost, entryUri: string, resolver: Resolver): Promise<MiNote | null | undefined> {
+ const quoteUris = new Set<string>();
+ if (note._misskey_quote) quoteUris.add(note._misskey_quote);
+ if (note.quoteUrl) quoteUris.add(note.quoteUrl);
+ if (note.quoteUri) quoteUris.add(note.quoteUri);
+
+ // No quote, return undefined
+ if (quoteUris.size < 1) return undefined;
+
+ /**
+ * Attempts to resolve a quote by URI.
+ * Returns the note if successful, true if there's a retryable error, and false if there's a permanent error.
+ */
+ const resolveQuote = async (uri: unknown): Promise<MiNote | boolean> => {
+ if (typeof(uri) !== 'string' || !/^https?:/.test(uri)) {
+ this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": URI is invalid`);
+ return false;
+ }
+
+ try {
+ const quote = await this.resolveNote(uri, { resolver });
+
+ if (quote == null) {
+ this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": request error`);
+ return false;
+ }
+
+ return quote;
+ } catch (e) {
+ if (e instanceof Error) {
+ this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}":`, e);
+ } else {
+ this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": ${e}`);
+ }
+
+ return isRetryableError(e);
+ }
+ };
+
+ const results = await Promise.all(Array.from(quoteUris).map(u => resolveQuote(u)));
+
+ // Success - return the quote
+ const quote = results.find(r => typeof(r) === 'object');
+ if (quote) return quote;
+
+ // Temporary / retryable error - throw error
+ const tempError = results.find(r => r === true);
+ if (tempError) throw new Error(`temporary error resolving quote for "${entryUri}"`);
+
+ // Permanent error - return null
+ return null;
+ }
+}
+
+function getBestIcon(note: IObject): IObject | null {
+ const icons: IObject[] = toArray(note.icon);
+ if (icons.length < 2) {
+ return icons[0] ?? null;
+ }
+
+ return icons.reduce((best, i) => {
+ if (!isApObject(i)) return best;
+ if (!isDocument(i)) return best;
+ if (!best) return i;
+ if (!best.width || !best.height) return i;
+ if (!i.width || !i.height) return best;
+ if (i.width > best.width) return i;
+ if (i.height > best.height) return i;
+ return best;
+ }, null as IApDocument | null) ?? null;
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 5c71dbc626..da29a3c527 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -39,8 +39,8 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.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 { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
+import { getApId, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
import type { ApNoteService } from './ApNoteService.js';
@@ -106,6 +106,7 @@ export class ApPersonService implements OnModuleInit {
private followingsRepository: FollowingsRepository,
private roleService: RoleService,
+ private readonly apUtilityService: ApUtilityService,
) {
}
@@ -346,21 +347,11 @@ export class ApPersonService implements OnModuleInit {
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
- const url = getOneApHrefNullable(person.url);
-
if (person.id == null) {
throw new UnrecoverableError(`Refusing to create person without id: ${uri}`);
}
- if (url != null) {
- if (!checkHttps(url)) {
- throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
- }
-
- if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) {
- throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
- }
- }
+ const url = this.apUtilityService.findBestObjectUrl(person);
// Create user
let user: MiRemoteUser | null = null;
@@ -398,7 +389,7 @@ export class ApPersonService implements OnModuleInit {
alsoKnownAs: person.alsoKnownAs,
// We use "!== false" to handle incorrect types, missing / null values, and "default to true" logic.
hideOnlineStatus: person.hideOnlineStatus !== false,
- isExplorable: person.discoverable,
+ isExplorable: person.discoverable !== false,
username: person.preferredUsername,
approved: true,
usernameLower: person.preferredUsername?.toLowerCase(),
@@ -447,7 +438,7 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new MiUserPublickey({
userId: user.id,
keyId: person.publicKey.id,
- keyPem: person.publicKey.publicKeyPem,
+ keyPem: person.publicKey.publicKeyPem.trim(),
}));
}
});
@@ -566,21 +557,11 @@ export class ApPersonService implements OnModuleInit {
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
- const url = getOneApHrefNullable(person.url);
-
if (person.id == null) {
throw new UnrecoverableError(`Refusing to update person without id: ${uri}`);
}
- if (url != null) {
- if (!checkHttps(url)) {
- throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
- }
-
- if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) {
- throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
- }
- }
+ const url = this.apUtilityService.findBestObjectUrl(person);
const updates = {
lastFetchedAt: new Date(),
@@ -602,7 +583,7 @@ export class ApPersonService implements OnModuleInit {
alsoKnownAs: person.alsoKnownAs ?? null,
// We use "!== false" to handle incorrect types, missing / null values, and "default to true" logic.
hideOnlineStatus: person.hideOnlineStatus !== false,
- isExplorable: person.discoverable,
+ isExplorable: person.discoverable !== false,
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image, person.backgroundUrl).catch(() => ({}))),
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'speakAsCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index d67f8cf62e..d8e7b3c9c3 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { UnrecoverableError } from 'bullmq';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
import { fromTuple } from '@/misc/from-tuple.js';
export type Obj = { [x: string]: any };
@@ -65,7 +65,7 @@ export function getApId(value: string | IObject | [string | IObject]): string {
if (typeof value === 'string') return value;
if (typeof value.id === 'string') return value.id;
- throw new UnrecoverableError('cannot determine id');
+ throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', `invalid AP object ${value}: missing id`);
}
/**
@@ -202,7 +202,7 @@ export interface IActor extends IObject {
manuallyApprovesFollowers?: boolean;
movedTo?: string;
alsoKnownAs?: string[];
- discoverable?: boolean;
+ discoverable?: boolean | null;
inbox: string;
sharedInbox?: string; // 後方互換性のため
publicKey?: {
@@ -270,6 +270,11 @@ export interface IApEmoji extends IObject {
type: 'Emoji';
name: string;
updated: string;
+ // Misskey拡張。後方互換性のためにoptional。
+ // 将来の拡張性を考慮してobjectにしている
+ _misskey_license?: {
+ freeText: string | null;
+ };
}
export const isEmoji = (object: IObject): object is IApEmoji =>
@@ -285,6 +290,8 @@ export const validDocumentTypes = ['Audio', 'Document', 'Image', 'Page', 'Video'
export interface IApDocument extends IObject {
type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
+ width?: number;
+ height?: number;
}
export const isDocument = (object: IObject): object is IApDocument => {
diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts
index ef65af2432..81495c8a6c 100644
--- a/packages/backend/src/core/chart/ChartManagementService.ts
+++ b/packages/backend/src/core/chart/ChartManagementService.ts
@@ -41,7 +41,7 @@ export class ChartManagementService implements OnApplicationShutdown {
private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart,
- private chartLoggerService: ChartLoggerService,
+ chartLoggerService: ChartLoggerService,
) {
this.charts = [
this.federationChart,
diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts
index 841bd731c0..490d3f2511 100644
--- a/packages/backend/src/core/entities/EmojiEntityService.ts
+++ b/packages/backend/src/core/entities/EmojiEntityService.ts
@@ -4,10 +4,10 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { EmojisRepository } from '@/models/_.js';
+import type { EmojisRepository, MiRole, RolesRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
import type { MiEmoji } from '@/models/Emoji.js';
import { bindThis } from '@/decorators.js';
@@ -16,6 +16,8 @@ export class EmojiEntityService {
constructor(
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
) {
}
@@ -68,8 +70,90 @@ export class EmojiEntityService {
@bindThis
public packDetailedMany(
emojis: any[],
- ) {
+ ): Promise<Packed<'EmojiDetailed'>[]> {
return Promise.all(emojis.map(x => this.packDetailed(x)));
}
+
+ @bindThis
+ public async packDetailedAdmin(
+ src: MiEmoji['id'] | MiEmoji,
+ hint?: {
+ roles?: Map<MiRole['id'], MiRole>
+ },
+ ): Promise<Packed<'EmojiDetailedAdmin'>> {
+ const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
+
+ const roles = Array.of<MiRole>();
+ if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0) {
+ if (hint?.roles) {
+ const hintRoles = hint.roles;
+ roles.push(
+ ...emoji.roleIdsThatCanBeUsedThisEmojiAsReaction
+ .filter(x => hintRoles.has(x))
+ .map(x => hintRoles.get(x)!),
+ );
+ } else {
+ roles.push(
+ ...await this.rolesRepository.findBy({ id: In(emoji.roleIdsThatCanBeUsedThisEmojiAsReaction) }),
+ );
+ }
+
+ roles.sort((a, b) => {
+ if (a.displayOrder !== b.displayOrder) {
+ return b.displayOrder - a.displayOrder;
+ }
+
+ return a.id.localeCompare(b.id);
+ });
+ }
+
+ return {
+ id: emoji.id,
+ updatedAt: emoji.updatedAt?.toISOString() ?? null,
+ name: emoji.name,
+ host: emoji.host,
+ uri: emoji.uri,
+ type: emoji.type,
+ aliases: emoji.aliases,
+ category: emoji.category,
+ publicUrl: emoji.publicUrl,
+ originalUrl: emoji.originalUrl,
+ license: emoji.license,
+ localOnly: emoji.localOnly,
+ isSensitive: emoji.isSensitive,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: roles.map(it => ({ id: it.id, name: it.name })),
+ };
+ }
+
+ @bindThis
+ public async packDetailedAdminMany(
+ emojis: MiEmoji['id'][] | MiEmoji[],
+ hint?: {
+ roles?: Map<MiRole['id'], MiRole>
+ },
+ ): Promise<Packed<'EmojiDetailedAdmin'>[]> {
+ // IDのみの要素をピックアップし、DBからレコードを取り出して他の値を補完する
+ const emojiEntities = emojis.filter(x => typeof x === 'object') as MiEmoji[];
+ const emojiIdOnlyList = emojis.filter(x => typeof x === 'string') as string[];
+ if (emojiIdOnlyList.length > 0) {
+ emojiEntities.push(...await this.emojisRepository.findBy({ id: In(emojiIdOnlyList) }));
+ }
+
+ // 特定ロール専用の絵文字である場合、そのロール情報をあらかじめまとめて取得しておく(pack側で都度取得も出来るが負荷が高いので)
+ let hintRoles: Map<MiRole['id'], MiRole>;
+ if (hint?.roles) {
+ hintRoles = hint.roles;
+ } else {
+ const roles = Array.of<MiRole>();
+ const roleIds = [...new Set(emojiEntities.flatMap(x => x.roleIdsThatCanBeUsedThisEmojiAsReaction))];
+ if (roleIds.length > 0) {
+ roles.push(...await this.rolesRepository.findBy({ id: In(roleIds) }));
+ }
+
+ hintRoles = new Map(roles.map(x => [x.id, x]));
+ }
+
+ return Promise.all(emojis.map(x => this.packDetailedAdmin(x, { roles: hintRoles })));
+ }
}
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index 63e5923255..fcc9bed3bd 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -60,6 +60,7 @@ export class InstanceEntityService {
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
isNSFW: instance.isNSFW,
rejectReports: instance.rejectReports,
+ rejectQuotes: instance.rejectQuotes,
moderationNote: iAmModerator ? instance.moderationNote : null,
};
}
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 7d7b4cbd81..84d591ce7a 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -95,6 +95,7 @@ export class MetaEntityService {
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
enableRecaptcha: instance.enableRecaptcha,
enableAchievements: instance.enableAchievements,
+ robotsTxt: instance.robotsTxt,
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
@@ -144,6 +145,7 @@ export class MetaEntityService {
enableUrlPreview: instance.urlPreviewEnabled,
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
maxFileSize: this.config.maxFileSize,
+ federation: this.meta.federation,
};
return packed;
@@ -184,3 +186,4 @@ export class MetaEntityService {
return packDetailed;
}
}
+
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index eb6b353752..537677ed34 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -23,7 +23,6 @@ import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
-import type { Config } from '@/config.js';
// is-renote.tsとよしなにリンク
function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
@@ -42,8 +41,14 @@ function getAppearNoteIds(notes: MiNote[]): Set<string> {
for (const note of notes) {
if (isPureRenote(note)) {
appearNoteIds.add(note.renoteId);
+ if (note.renote?.replyId) {
+ appearNoteIds.add(note.renote.replyId);
+ }
} else {
appearNoteIds.add(note.id);
+ if (note.replyId) {
+ appearNoteIds.add(note.replyId);
+ }
}
}
return appearNoteIds;
@@ -69,9 +74,6 @@ export class NoteEntityService implements OnModuleInit {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- @Inject(DI.config)
- private config: Config,
-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -110,8 +112,7 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
- private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
- // FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある)
+ private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
if ((followersOnlyBefore != null)
@@ -123,7 +124,11 @@ export class NoteEntityService implements OnModuleInit {
packedNote.visibility = 'followers';
}
}
+ return packedNote.visibility;
+ }
+ @bindThis
+ public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
if (meId === packedNote.userId) return;
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
@@ -272,7 +277,9 @@ export class NoteEntityService implements OnModuleInit {
const reaction = _hint_.myReactions.get(note.id);
if (reaction) {
return this.reactionService.convertLegacyReaction(reaction);
- } else {
+ } else if (reaction === null) {
+ // the hints explicitly say this note has no reactions from
+ // this user
return undefined;
}
}
@@ -483,6 +490,7 @@ export class NoteEntityService implements OnModuleInit {
...(opts.detail ? {
clippedCount: note.clippedCount,
+ processErrors: note.processErrors,
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
detail: false,
@@ -500,6 +508,8 @@ export class NoteEntityService implements OnModuleInit {
} : {}),
});
+ this.treatVisibility(packed);
+
if (!opts.skipHide) {
await this.hideNote(packed, meId);
}
@@ -525,44 +535,39 @@ export class NoteEntityService implements OnModuleInit {
if (meId) {
const idsNeedFetchMyReaction = new Set<MiNote['id']>();
- // パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
- const oldId = this.idService.gen(Date.now() - 2000);
-
+ const targetNotes: MiNote[] = [];
for (const note of notes) {
if (isPureRenote(note)) {
- const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
- if (reactionsCount === 0) {
- myReactionsMap.set(note.renote.id, null);
- } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
- const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
- if (pairInBuffer) {
- myReactionsMap.set(note.renote.id, pairInBuffer[1]);
- } else {
- const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
- myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
- }
- } else {
- idsNeedFetchMyReaction.add(note.renote.id);
+ // we may need to fetch 'my reaction' for renote target.
+ targetNotes.push(note.renote);
+ if (note.renote.reply) {
+ // idem if the renote is also a reply.
+ targetNotes.push(note.renote.reply);
}
} else {
- if (note.id < oldId) {
- const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
- if (reactionsCount === 0) {
- myReactionsMap.set(note.id, null);
- } else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
- const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
- if (pairInBuffer) {
- myReactionsMap.set(note.id, pairInBuffer[1]);
- } else {
- const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
- myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
- }
- } else {
- idsNeedFetchMyReaction.add(note.id);
- }
+ if (note.reply) {
+ // idem for OP of a regular reply.
+ targetNotes.push(note.reply);
+ }
+
+ targetNotes.push(note);
+ }
+ }
+
+ for (const note of targetNotes) {
+ const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
+ if (reactionsCount === 0) {
+ myReactionsMap.set(note.id, null);
+ } else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
+ const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
+ if (pairInBuffer) {
+ myReactionsMap.set(note.id, pairInBuffer[1]);
} else {
- myReactionsMap.set(note.id, null);
+ const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
+ myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
}
+ } else {
+ idsNeedFetchMyReaction.add(note.id);
}
}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 6bfe865038..96fef863a0 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -49,11 +49,13 @@ import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { isSystemAccount } from '@/misc/is-system-account.js';
import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
-import { isSystemAccount } from '@/misc/is-system-account.js';
+
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
const Ajv = _Ajv.default;
const ajv = new Ajv();
@@ -81,6 +83,8 @@ export type UserRelation = {
isBlocked: boolean
isMuted: boolean
isRenoteMuted: boolean
+ isInstanceMuted?: boolean
+ memo?: string | null
}
@Injectable()
@@ -180,6 +184,9 @@ export class UserEntityService implements OnModuleInit {
isBlocked,
isMuted,
isRenoteMuted,
+ host,
+ memo,
+ mutedInstances,
] = await Promise.all([
this.followingsRepository.findOneBy({
followerId: me,
@@ -227,8 +234,25 @@ export class UserEntityService implements OnModuleInit {
muteeId: target,
},
}),
+ this.usersRepository.createQueryBuilder('u')
+ .select('u.host')
+ .where({ id: target })
+ .getRawOne<{ u_host: string }>()
+ .then(it => it?.u_host ?? null),
+ this.userMemosRepository.createQueryBuilder('m')
+ .select('m.memo')
+ .where({ userId: me, targetUserId: target })
+ .getRawOne<{ m_memo: string | null }>()
+ .then(it => it?.m_memo ?? null),
+ this.userProfilesRepository.createQueryBuilder('p')
+ .select('p.mutedInstances')
+ .where({ userId: me })
+ .getRawOne<{ p_mutedInstances: string[] }>()
+ .then(it => it?.p_mutedInstances ?? []),
]);
+ const isInstanceMuted = !!host && mutedInstances.includes(host);
+
return {
id: target,
following,
@@ -240,6 +264,8 @@ export class UserEntityService implements OnModuleInit {
isBlocked,
isMuted,
isRenoteMuted,
+ isInstanceMuted,
+ memo,
};
}
@@ -254,6 +280,9 @@ export class UserEntityService implements OnModuleInit {
blockees,
muters,
renoteMuters,
+ hosts,
+ memos,
+ mutedInstances,
] = await Promise.all([
this.followingsRepository.findBy({ followerId: me })
.then(f => new Map(f.map(it => [it.followeeId, it]))),
@@ -292,6 +321,27 @@ export class UserEntityService implements OnModuleInit {
.where('m.muterId = :me', { me })
.getRawMany<{ m_muteeId: string }>()
.then(it => it.map(it => it.m_muteeId)),
+ this.usersRepository.createQueryBuilder('u')
+ .select(['u.id', 'u.host'])
+ .where({ id: In(targets) } )
+ .getRawMany<{ m_id: string, m_host: string }>()
+ .then(it => it.reduce((map, it) => {
+ map[it.m_id] = it.m_host;
+ return map;
+ }, {} as Record<string, string>)),
+ this.userMemosRepository.createQueryBuilder('m')
+ .select(['m.targetUserId', 'm.memo'])
+ .where({ userId: me, targetUserId: In(targets) })
+ .getRawMany<{ m_targetUserId: string, m_memo: string | null }>()
+ .then(it => it.reduce((map, it) => {
+ map[it.m_targetUserId] = it.m_memo;
+ return map;
+ }, {} as Record<string, string | null>)),
+ this.userProfilesRepository.createQueryBuilder('p')
+ .select('p.mutedInstances')
+ .where({ userId: me })
+ .getRawOne<{ p_mutedInstances: string[] }>()
+ .then(it => it?.p_mutedInstances ?? []),
]);
return new Map(
@@ -311,6 +361,8 @@ export class UserEntityService implements OnModuleInit {
isBlocked: blockees.includes(target),
isMuted: muters.includes(target),
isRenoteMuted: renoteMuters.includes(target),
+ isInstanceMuted: mutedInstances.includes(hosts[target]),
+ memo: memos[target] ?? null,
},
];
}),
@@ -540,6 +592,8 @@ export class UserEntityService implements OnModuleInit {
isCat: user.isCat,
noindex: user.noindex,
enableRss: user.enableRss,
+ mandatoryCW: user.mandatoryCW,
+ rejectQuotes: user.rejectQuotes,
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
speakAsCat: user.speakAsCat ?? false,
approved: user.approved,
@@ -669,6 +723,8 @@ export class UserEntityService implements OnModuleInit {
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
policies: this.roleService.getUserPolicies(user.id),
+ defaultCW: profile!.defaultCW,
+ defaultCWPriority: profile!.defaultCWPriority,
} : {}),
...(opts.includeSecrets ? {
diff --git a/packages/backend/src/daemons/ApLogCleanupService.ts b/packages/backend/src/daemons/ApLogCleanupService.ts
new file mode 100644
index 0000000000..61f76b4e2c
--- /dev/null
+++ b/packages/backend/src/daemons/ApLogCleanupService.ts
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable, type OnApplicationShutdown } from '@nestjs/common';
+import { bindThis } from '@/decorators.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import Logger from '@/logger.js';
+import { ApLogService } from '@/core/ApLogService.js';
+
+// 10 minutes
+export const scanInterval = 1000 * 60 * 10;
+
+@Injectable()
+export class ApLogCleanupService implements OnApplicationShutdown {
+ private readonly logger: Logger;
+ private scanTimer: NodeJS.Timeout | null = null;
+
+ constructor(
+ private readonly apLogService: ApLogService,
+ loggerService: LoggerService,
+ ) {
+ this.logger = loggerService.getLogger('activity-log-cleanup');
+ }
+
+ @bindThis
+ public async start(): Promise<void> {
+ // Just in case start() gets called multiple times.
+ this.dispose();
+
+ // Prune at startup, in case the server was rebooted during the interval.
+ // noinspection ES6MissingAwait
+ this.tick();
+
+ // Prune on a regular interval for the lifetime of the server.
+ this.scanTimer = setInterval(this.tick, scanInterval);
+ }
+
+ @bindThis
+ private async tick(): Promise<void> {
+ try {
+ const affected = await this.apLogService.deleteExpiredLogs();
+ this.logger.info(`Activity Log cleanup complete; removed ${affected} expired logs.`);
+ } catch (err) {
+ this.logger.error('Activity Log cleanup failed:', err as Error);
+ }
+ }
+
+ @bindThis
+ public onApplicationShutdown(): void {
+ this.dispose();
+ }
+
+ @bindThis
+ public dispose(): void {
+ if (this.scanTimer) {
+ clearInterval(this.scanTimer);
+ this.scanTimer = null;
+ }
+ }
+}
diff --git a/packages/backend/src/daemons/DaemonModule.ts b/packages/backend/src/daemons/DaemonModule.ts
index a67907e6dd..ea71875f19 100644
--- a/packages/backend/src/daemons/DaemonModule.ts
+++ b/packages/backend/src/daemons/DaemonModule.ts
@@ -8,6 +8,7 @@ import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js';
import { QueueStatsService } from './QueueStatsService.js';
import { ServerStatsService } from './ServerStatsService.js';
+import { ApLogCleanupService } from './ApLogCleanupService.js';
@Module({
imports: [
@@ -17,10 +18,12 @@ import { ServerStatsService } from './ServerStatsService.js';
providers: [
QueueStatsService,
ServerStatsService,
+ ApLogCleanupService,
],
exports: [
QueueStatsService,
ServerStatsService,
+ ApLogCleanupService,
],
})
export class DaemonModule {}
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 296cc4815b..461fcf11c3 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -13,6 +13,7 @@ export const DI = {
redisForSub: Symbol('redisForSub'),
redisForTimelines: Symbol('redisForTimelines'),
redisForReactions: Symbol('redisForReactions'),
+ redisForRateLimit: Symbol('redisForRateLimit'),
//#region Repositories
usersRepository: Symbol('usersRepository'),
@@ -22,6 +23,9 @@ export const DI = {
appsRepository: Symbol('appsRepository'),
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
latestNotesRepository: Symbol('latestNotesRepository'),
+ apContextsRepository: Symbol('apContextsRepository'),
+ apFetchLogsRepository: Symbol('apFetchLogsRepository'),
+ apInboxLogsRepository: Symbol('apInboxLogsRepository'),
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
noteReactionsRepository: Symbol('noteReactionsRepository'),
diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts
index 3b20ae5df0..eb2b081220 100644
--- a/packages/backend/src/logger.ts
+++ b/packages/backend/src/logger.ts
@@ -18,8 +18,8 @@ type Context = {
type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
-type Data = DataElement | DataElement[];
-type DataElement = Record<string, unknown> | Error | string | null;
+export type Data = DataElement | DataElement[];
+export type DataElement = Record<string, unknown> | Error | string | null;
// eslint-disable-next-line import/no-default-export
export default class Logger {
diff --git a/packages/backend/src/misc/append-content-warning.ts b/packages/backend/src/misc/append-content-warning.ts
new file mode 100644
index 0000000000..152cd6760e
--- /dev/null
+++ b/packages/backend/src/misc/append-content-warning.ts
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/*
+ * Important Note: this file must be kept in sync with packages/frontend-shared/js/append-content-warning.ts
+ */
+
+/**
+ * Appends an additional content warning onto an existing one.
+ * The additional value will not be added if it already exists within the original input.
+ * @param original Existing content warning
+ * @param additional Content warning to append
+ * @param reverse If true, then the additional CW will be prepended instead of appended.
+ */
+export function appendContentWarning(original: string | null | undefined, additional: string, reverse = false): string {
+ // Easy case - if original is empty, then additional replaces it.
+ if (!original) {
+ return additional;
+ }
+
+ // Easy case - if the additional CW is empty, then don't append it.
+ if (!additional) {
+ return original;
+ }
+
+ // If the additional CW already exists in the input, then we *don't* append another copy!
+ if (includesWholeWord(original, additional)) {
+ return original;
+ }
+
+ return reverse
+ ? `${additional}, ${original}`
+ : `${original}, ${additional}`;
+}
+
+/**
+ * Emulates a regular expression like /\b(pattern)\b/, but with a raw non-regex pattern.
+ * We're checking to see whether the default CW appears inside the existing CW, but *only* if there's word boundaries on either side.
+ * @param input Input string to search
+ * @param target Target word / phrase to search for
+ */
+function includesWholeWord(input: string, target: string): boolean {
+ const parts = input.split(target);
+
+ // The additional string could appear multiple times within the original input.
+ // We need to check each occurrence, since any of them could potentially match.
+ for (let i = 0; i + 1 < parts.length; i++) {
+ const before = parts[i];
+ const after = parts[i + 1];
+
+ // If either the preceding or following tokens are a "word", then this "match" is actually just part of a longer word.
+ // Likewise, if *neither* token is a word, then this is a real match and the CW already exists in the input.
+ if (!/\w$/.test(before) && !/^\w/.test(after)) {
+ return true;
+ }
+ }
+
+ // If we don't match, then there is no existing CW.
+ return false;
+}
diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts
index 342e0f8602..f3c08cc76e 100644
--- a/packages/backend/src/misc/gen-identicon.ts
+++ b/packages/backend/src/misc/gen-identicon.ts
@@ -8,7 +8,7 @@
* https://en.wikipedia.org/wiki/Identicon
*/
-import { createCanvas } from '@napi-rs/canvas';
+import { createCanvas } from 'canvas';
import gen from 'random-seed';
const size = 128; // px
@@ -100,5 +100,5 @@ export async function genIdenticon(seed: string): Promise<Buffer> {
}
}
- return await canvas.encode('png');
+ return await canvas.toBuffer('image/png');
}
diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts
index 60dddee9a2..be2d3ea98d 100644
--- a/packages/backend/src/misc/get-note-summary.ts
+++ b/packages/backend/src/misc/get-note-summary.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { appendContentWarning } from './append-content-warning.js';
import type { Packed } from './json-schema.js';
/**
@@ -20,9 +21,15 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
let summary = '';
+ // Append mandatory CW, if applicable
+ let cw = note.cw;
+ if (note.user.mandatoryCW) {
+ cw = appendContentWarning(cw, note.user.mandatoryCW);
+ }
+
// 本文
- if (note.cw != null) {
- summary += `CW: ${note.cw}`;
+ if (cw != null) {
+ summary += `CW: ${cw}`;
} else if (note.text) {
summary += note.text;
}
diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts
index 13c41f1e3b..f5c3fcd6cb 100644
--- a/packages/backend/src/misc/identifiable-error.ts
+++ b/packages/backend/src/misc/identifiable-error.ts
@@ -10,9 +10,15 @@ export class IdentifiableError extends Error {
public message: string;
public id: string;
- constructor(id: string, message?: string) {
+ /**
+ * Indicates that this is a temporary error that may be cleared by retrying
+ */
+ public readonly isRetryable: boolean;
+
+ constructor(id: string, message?: string, isRetryable = false) {
super(message);
this.message = message ?? '';
this.id = id;
+ this.isRetryable = isRetryable;
}
}
diff --git a/packages/backend/src/misc/is-retryable-error.ts b/packages/backend/src/misc/is-retryable-error.ts
new file mode 100644
index 0000000000..9bb8700c7a
--- /dev/null
+++ b/packages/backend/src/misc/is-retryable-error.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { AbortError } from 'node-fetch';
+import { UnrecoverableError } from 'bullmq';
+import { StatusError } from '@/misc/status-error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+
+/**
+ * Returns false if the provided value represents a "permanent" error that cannot be retried.
+ * Returns true if the error is retryable, unknown (as all errors are retryable by default), or not an error object.
+ */
+export function isRetryableError(e: unknown): boolean {
+ if (e instanceof StatusError) return e.isRetryable;
+ if (e instanceof IdentifiableError) return e.isRetryable;
+ if (e instanceof UnrecoverableError) return false;
+ if (e instanceof AbortError) return true;
+ if (e instanceof Error) return e.name === 'AbortError';
+ return true;
+}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 040e36228c..f612591eda 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -33,7 +33,11 @@ import { packedClipSchema } from '@/models/json-schema/clip.js';
import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js';
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
-import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
+import {
+ packedEmojiDetailedAdminSchema,
+ packedEmojiDetailedSchema,
+ packedEmojiSimpleSchema,
+} from '@/models/json-schema/emoji.js';
import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js';
@@ -95,6 +99,7 @@ export const refs = {
GalleryPost: packedGalleryPostSchema,
EmojiSimple: packedEmojiSimpleSchema,
EmojiDetailed: packedEmojiDetailedSchema,
+ EmojiDetailedAdmin: packedEmojiDetailedAdminSchema,
Flash: packedFlashSchema,
Signin: packedSigninSchema,
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts
index ba93190c57..c64ebb1b3b 100644
--- a/packages/backend/src/models/Instance.ts
+++ b/packages/backend/src/models/Instance.ts
@@ -164,6 +164,15 @@ export class MiInstance {
})
public rejectReports: boolean;
+ /**
+ * If true, quote posts from this instance will be downgraded to normal posts.
+ * The quote will be stripped and a process error will be generated.
+ */
+ @Column('boolean', {
+ default: false,
+ })
+ public rejectQuotes: boolean;
+
@Column('varchar', {
length: 16384, default: '',
})
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 3fc3f273dd..a224117676 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -599,6 +599,11 @@ export class MiMeta {
})
public enableAchievements: boolean;
+ @Column('varchar', {
+ length: 2048, nullable: true,
+ })
+ public robotsTxt: string | null;
+
@Column('jsonb', {
default: { },
})
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index 408e023ff7..2dabb75d83 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -143,6 +143,7 @@ export class MiNote {
})
public fileIds: MiDriveFile['id'][];
+ @Index('IDX_NOTE_ATTACHED_FILE_TYPES', { synchronize: false })
@Column('varchar', {
length: 256, array: true, default: '{}',
})
@@ -202,6 +203,17 @@ export class MiNote {
@JoinColumn()
public channel: MiChannel | null;
+ /**
+ * List of non-fatal errors encountered while processing (creating or updating) this note.
+ * Entries can be a translation key (which will be queried from the "_processErrors" section) or a raw string.
+ * Errors will be displayed to the user when viewing the note.
+ */
+ @Column('text', {
+ array: true,
+ nullable: true,
+ })
+ public processErrors: string[] | null;
+
//#region Denormalized fields
@Index()
@Column('varchar', {
diff --git a/packages/backend/src/models/Page.ts b/packages/backend/src/models/Page.ts
index 1695bf570e..0b59e7a92c 100644
--- a/packages/backend/src/models/Page.ts
+++ b/packages/backend/src/models/Page.ts
@@ -118,3 +118,5 @@ export class MiPage {
}
}
}
+
+export const pageNameSchema = { type: 'string', pattern: /^[^\s:\/?#\[\]@!$&'()*+,;=\\%\x00-\x20]{1,256}$/.source } as const;
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 3a1158a42a..78510ba588 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -80,7 +80,10 @@ import {
MiUserPublickey,
MiUserSecurityKey,
MiWebhook,
- NoteEdit
+ NoteEdit,
+ SkApContext,
+ SkApFetchLog,
+ SkApInboxLog,
} from './_.js';
import type { DataSource } from 'typeorm';
@@ -126,6 +129,24 @@ const $latestNotesRepository: Provider = {
inject: [DI.db],
};
+const $apContextRepository: Provider = {
+ provide: DI.apContextsRepository,
+ useFactory: (db: DataSource) => db.getRepository(SkApContext).extend(miRepository as MiRepository<SkApContext>),
+ inject: [DI.db],
+};
+
+const $apFetchLogsRepository: Provider = {
+ provide: DI.apFetchLogsRepository,
+ useFactory: (db: DataSource) => db.getRepository(SkApFetchLog).extend(miRepository as MiRepository<SkApFetchLog>),
+ inject: [DI.db],
+};
+
+const $apInboxLogsRepository: Provider = {
+ provide: DI.apInboxLogsRepository,
+ useFactory: (db: DataSource) => db.getRepository(SkApInboxLog).extend(miRepository as MiRepository<SkApInboxLog>),
+ inject: [DI.db],
+};
+
const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>),
@@ -526,6 +547,9 @@ const $noteScheduleRepository: Provider = {
$appsRepository,
$avatarDecorationsRepository,
$latestNotesRepository,
+ $apContextRepository,
+ $apFetchLogsRepository,
+ $apInboxLogsRepository,
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
@@ -600,6 +624,9 @@ const $noteScheduleRepository: Provider = {
$appsRepository,
$avatarDecorationsRepository,
$latestNotesRepository,
+ $apContextRepository,
+ $apFetchLogsRepository,
+ $apInboxLogsRepository,
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
diff --git a/packages/backend/src/models/SkApContext.ts b/packages/backend/src/models/SkApContext.ts
new file mode 100644
index 0000000000..ff4c6d6fbf
--- /dev/null
+++ b/packages/backend/src/models/SkApContext.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, PrimaryColumn, Entity } from 'typeorm';
+
+@Entity('ap_context')
+export class SkApContext {
+ @PrimaryColumn('text', {
+ primaryKeyConstraintName: 'PK_ap_context',
+ })
+ public md5: string;
+
+ @Column('jsonb')
+ // https://github.com/typeorm/typeorm/issues/8559
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public json: any;
+
+ constructor(data?: Partial<SkApContext>) {
+ if (data) {
+ Object.assign(this, data);
+ }
+ }
+}
diff --git a/packages/backend/src/models/SkApFetchLog.ts b/packages/backend/src/models/SkApFetchLog.ts
new file mode 100644
index 0000000000..1e7d861b6c
--- /dev/null
+++ b/packages/backend/src/models/SkApFetchLog.ts
@@ -0,0 +1,89 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Index, JoinColumn, ManyToOne, PrimaryColumn, Entity } from 'typeorm';
+import { SkApContext } from '@/models/SkApContext.js';
+import { id } from './util/id.js';
+
+/**
+ * Records objects fetched via AP
+ */
+@Entity('ap_fetch_log')
+export class SkApFetchLog {
+ @PrimaryColumn({
+ ...id(),
+ primaryKeyConstraintName: 'PK_ap_fetch_log',
+ })
+ public id: string;
+
+ @Index('IDX_ap_fetch_log_at')
+ @Column('timestamptz')
+ public at: Date;
+
+ /**
+ * Processing duration in milliseconds
+ */
+ @Column('double precision', { nullable: true })
+ public duration: number | null = null;
+
+ /**
+ * DB hostname extracted from responseUri, or requestUri if fetch is incomplete
+ */
+ @Index('IDX_ap_fetch_log_host')
+ @Column('text')
+ public host: string;
+
+ /**
+ * Original requested URI
+ */
+ @Column('text', {
+ name: 'request_uri',
+ })
+ public requestUri: string;
+
+ /**
+ * Canonical URI / object ID, taken from the final payload
+ */
+ @Column('text', {
+ name: 'object_uri',
+ nullable: true,
+ })
+ @Index('IDX_ap_fetch_log_object_uri')
+ public objectUri: string | null = null;
+
+ @Column('boolean', { nullable: true })
+ public accepted: boolean | null = null;
+
+ @Column('text', { nullable: true })
+ public result: string | null = null;
+
+ @Column('jsonb', { nullable: true })
+ // https://github.com/typeorm/typeorm/issues/8559
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public object: any | null = null;
+
+ @Column({
+ type: 'text',
+ name: 'context_hash',
+ nullable: true,
+ })
+ public contextHash: string | null;
+
+ @ManyToOne(() => SkApContext, {
+ onDelete: 'CASCADE',
+ nullable: true,
+ })
+ @JoinColumn({
+ name: 'context_hash',
+ foreignKeyConstraintName: 'FK_ap_fetch_log_context_hash',
+ })
+ public context: SkApContext | null;
+
+ constructor(data?: Partial<SkApFetchLog>) {
+ if (data) {
+ Object.assign(this, data);
+ }
+ }
+}
diff --git a/packages/backend/src/models/SkApInboxLog.ts b/packages/backend/src/models/SkApInboxLog.ts
new file mode 100644
index 0000000000..867094405c
--- /dev/null
+++ b/packages/backend/src/models/SkApInboxLog.ts
@@ -0,0 +1,109 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { SkApContext } from '@/models/SkApContext.js';
+import { MiUser } from '@/models/_.js';
+import { id } from './util/id.js';
+
+/**
+ * Records activities received in the inbox
+ */
+@Entity('ap_inbox_log')
+export class SkApInboxLog {
+ @PrimaryColumn({
+ ...id(),
+ primaryKeyConstraintName: 'PK_ap_inbox_log',
+ })
+ public id: string;
+
+ @Index('IDX_ap_inbox_log_at')
+ @Column('timestamptz')
+ public at: Date;
+
+ /**
+ * Processing duration in milliseconds
+ */
+ @Column('double precision', { nullable: true })
+ public duration: number | null = null;
+
+ /**
+ * Key ID that was used to sign this request.
+ * Untrusted unless verified is true.
+ */
+ @Column({
+ type: 'text',
+ name: 'key_id',
+ })
+ public keyId: string;
+
+ /**
+ * Instance that the activity was sent from.
+ * Untrusted unless verified is true.
+ */
+ @Index('IDX_ap_inbox_log_host')
+ @Column('text')
+ public host: string;
+
+ @Column('boolean')
+ public verified: boolean;
+
+ @Column('boolean')
+ public accepted: boolean;
+
+ @Column('text', { nullable: true })
+ public result: string | null = null;
+
+ @Column('jsonb')
+ // https://github.com/typeorm/typeorm/issues/8559
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public activity: any;
+
+ @Column({
+ type: 'text',
+ name: 'context_hash',
+ nullable: true,
+ })
+ public contextHash: string | null;
+
+ @ManyToOne(() => SkApContext, {
+ onDelete: 'CASCADE',
+ nullable: true,
+ })
+ @JoinColumn({
+ name: 'context_hash',
+ foreignKeyConstraintName: 'FK_ap_inbox_log_context_hash',
+ })
+ public context: SkApContext | null;
+
+ /**
+ * ID of the user who signed this request.
+ */
+ @Column({
+ ...id(),
+ name: 'auth_user_id',
+ nullable: true,
+ })
+ public authUserId: string | null;
+
+ /**
+ * User who signed this request.
+ */
+ @ManyToOne(() => MiUser, {
+ onDelete: 'CASCADE',
+ nullable: true,
+ })
+ @JoinColumn({
+ name: 'auth_user_id',
+ foreignKeyConstraintName: 'FK_ap_inbox_log_auth_user_id',
+ })
+ public authUser: MiUser | null;
+
+ constructor(data?: Partial<SkApInboxLog>) {
+ if (data) {
+ Object.assign(this, data);
+ }
+ }
+}
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 3a825d36a7..5d87c7fa12 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -339,6 +339,24 @@ export class MiUser {
})
public enableRss: boolean;
+ /**
+ * Specifies a Content Warning that should be forcibly applied to all notes by this user.
+ * If null (default), then no Content Warning is applied.
+ */
+ @Column('text', {
+ nullable: true,
+ })
+ public mandatoryCW: string | null;
+
+ /**
+ * If true, quote posts from this user will be downgraded to normal posts.
+ * The quote will be stripped and a process error will be generated.
+ */
+ @Column('boolean', {
+ default: false,
+ })
+ public rejectQuotes: boolean;
+
constructor(data: Partial<MiUser>) {
if (data == null) return;
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 751b1aff08..449c2f370b 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -4,7 +4,7 @@
*/
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
-import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
+import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes, noteVisibilities, defaultCWPriorities } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiPage } from './Page.js';
@@ -36,10 +36,10 @@ export class MiUserProfile {
})
public birthday: string | null;
- @Column("varchar", {
+ @Column('varchar', {
length: 128,
nullable: true,
- comment: "The ListenBrainz username of the User.",
+ comment: 'The ListenBrainz username of the User.',
})
public listenbrainz: string | null;
@@ -290,6 +290,19 @@ export class MiUserProfile {
unlockedAt: number;
}[];
+ @Column('text', {
+ name: 'default_cw',
+ nullable: true,
+ })
+ public defaultCW: string | null;
+
+ @Column('enum', {
+ name: 'default_cw_priority',
+ enum: defaultCWPriorities,
+ default: 'parent',
+ })
+ public defaultCWPriority: typeof defaultCWPriorities[number];
+
//#region Denormalized fields
@Index()
@Column('varchar', {
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 9a4ebfc90f..4bd6e78ef4 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -82,6 +82,9 @@ import { NoteEdit } from '@/models/NoteEdit.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
import { MiNoteSchedule } from '@/models/NoteSchedule.js';
+import { SkApInboxLog } from '@/models/SkApInboxLog.js';
+import { SkApFetchLog } from '@/models/SkApFetchLog.js';
+import { SkApContext } from '@/models/SkApContext.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
export interface MiRepository<T extends ObjectLiteral> {
@@ -129,6 +132,9 @@ export const miRepository = {
export {
SkLatestNote,
+ SkApContext,
+ SkApFetchLog,
+ SkApInboxLog,
MiAbuseUserReport,
MiAbuseReportNotificationRecipient,
MiAccessToken,
@@ -229,6 +235,9 @@ export type HashtagsRepository = Repository<MiHashtag> & MiRepository<MiHashtag>
export type InstancesRepository = Repository<MiInstance> & MiRepository<MiInstance>;
export type MetasRepository = Repository<MiMeta> & MiRepository<MiMeta>;
export type LatestNotesRepository = Repository<SkLatestNote> & MiRepository<SkLatestNote>;
+export type ApContextsRepository = Repository<SkApContext> & MiRepository<SkApContext>;
+export type ApFetchLogsRepository = Repository<SkApFetchLog> & MiRepository<SkApFetchLog>;
+export type ApInboxLogsRepository = Repository<SkApInboxLog> & MiRepository<SkApInboxLog>;
export type ModerationLogsRepository = Repository<MiModerationLog> & MiRepository<MiModerationLog>;
export type MutingsRepository = Repository<MiMuting> & MiRepository<MiMuting>;
export type RenoteMutingsRepository = Repository<MiRenoteMuting> & MiRepository<MiRenoteMuting>;
diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts
index 62686ad5ae..3cd263fa37 100644
--- a/packages/backend/src/models/json-schema/emoji.ts
+++ b/packages/backend/src/models/json-schema/emoji.ts
@@ -104,3 +104,86 @@ export const packedEmojiDetailedSchema = {
},
},
} as const;
+
+export const packedEmojiDetailedAdminSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ optional: false, nullable: false,
+ },
+ updatedAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: true,
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ host: {
+ type: 'string',
+ optional: false, nullable: true,
+ description: 'The local host is represented with `null`.',
+ },
+ publicUrl: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ originalUrl: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ uri: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ type: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ aliases: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ format: 'id',
+ optional: false, nullable: false,
+ },
+ },
+ category: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ license: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ localOnly: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ isSensitive: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'misskey:id',
+ optional: false, nullable: false,
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ },
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts
index 7960e748e9..57d4466ffa 100644
--- a/packages/backend/src/models/json-schema/federation-instance.ts
+++ b/packages/backend/src/models/json-schema/federation-instance.ts
@@ -126,6 +126,11 @@ export const packedFederationInstanceSchema = {
optional: false,
nullable: false,
},
+ rejectQuotes: {
+ type: 'boolean',
+ optional: false,
+ nullable: false,
+ },
moderationNote: {
type: 'string',
optional: true, nullable: true,
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 5179e5d51c..bf68208c37 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -139,6 +139,10 @@ export const packedMetaLiteSchema = {
type: 'boolean',
optional: false, nullable: true,
},
+ robotsTxt: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
enableTestcaptcha: {
type: 'boolean',
optional: false, nullable: false,
@@ -317,6 +321,11 @@ export const packedMetaLiteSchema = {
type: 'number',
optional: false, nullable: false,
},
+ federation: {
+ type: 'string',
+ enum: ['all', 'specified', 'none'],
+ optional: false, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts
index 432c096e48..16e240ab11 100644
--- a/packages/backend/src/models/json-schema/note.ts
+++ b/packages/backend/src/models/json-schema/note.ts
@@ -17,6 +17,11 @@ export const packedNoteSchema = {
optional: false, nullable: false,
format: 'date-time',
},
+ updatedAt: {
+ type: 'string',
+ optional: true, nullable: false,
+ format: 'date-time',
+ },
deletedAt: {
type: 'string',
optional: true, nullable: true,
@@ -256,6 +261,14 @@ export const packedNoteSchema = {
type: 'number',
optional: true, nullable: false,
},
+ processErrors: {
+ type: 'array',
+ optional: true, nullable: true,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
myReaction: {
type: 'string',
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index f953008b3f..0f1601f138 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -134,6 +134,14 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: false,
},
+ mandatoryCW: {
+ type: 'string',
+ nullable: true, optional: false,
+ },
+ rejectQuotes: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
isBot: {
type: 'boolean',
nullable: false, optional: true,
@@ -752,6 +760,15 @@ export const packedMeDetailedOnlySchema = {
},
},
//#endregion
+ defaultCW: {
+ type: 'string',
+ nullable: true, optional: false,
+ },
+ defaultCWPriority: {
+ type: 'string',
+ enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
+ nullable: false, optional: false,
+ },
},
} as const;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index c964c3ffee..1a5fdc8412 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -85,6 +85,9 @@ import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { SkLatestNote } from '@/models/LatestNote.js';
+import { SkApContext } from '@/models/SkApContext.js';
+import { SkApFetchLog } from '@/models/SkApFetchLog.js';
+import { SkApInboxLog } from '@/models/SkApInboxLog.js';
pg.types.setTypeParser(20, Number);
@@ -92,27 +95,65 @@ export const dbLogger = new MisskeyLogger('db');
const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
+export type LoggerProps = {
+ disableQueryTruncation?: boolean;
+ enableQueryParamLogging?: boolean;
+}
+
+function highlightSql(sql: string) {
+ return highlight.highlight(sql, {
+ language: 'sql', ignoreIllegals: true,
+ });
+}
+
+function truncateSql(sql: string) {
+ return sql.length > 100 ? `${sql.substring(0, 100)}...` : sql;
+}
+
+function stringifyParameter(param: any) {
+ if (param instanceof Date) {
+ return param.toISOString();
+ } else {
+ return param;
+ }
+}
+
class MyCustomLogger implements Logger {
+ constructor(private props: LoggerProps = {}) {
+ }
+
+ @bindThis
+ private transformQueryLog(sql: string) {
+ let modded = sql;
+ if (!this.props.disableQueryTruncation) {
+ modded = truncateSql(modded);
+ }
+
+ return highlightSql(modded);
+ }
+
@bindThis
- private highlight(sql: string) {
- return highlight.highlight(sql, {
- language: 'sql', ignoreIllegals: true,
- });
+ private transformParameters(parameters?: any[]) {
+ if (this.props.enableQueryParamLogging && parameters && parameters.length > 0) {
+ return parameters.map(stringifyParameter);
+ }
+
+ return undefined;
}
@bindThis
public logQuery(query: string, parameters?: any[]) {
- sqlLogger.info(this.highlight(query).substring(0, 100));
+ sqlLogger.info(this.transformQueryLog(query), this.transformParameters(parameters));
}
@bindThis
public logQueryError(error: string, query: string, parameters?: any[]) {
- sqlLogger.error(this.highlight(query));
+ sqlLogger.error(this.transformQueryLog(query), this.transformParameters(parameters));
}
@bindThis
public logQuerySlow(time: number, query: string, parameters?: any[]) {
- sqlLogger.warn(this.highlight(query));
+ sqlLogger.warn(this.transformQueryLog(query), this.transformParameters(parameters));
}
@bindThis
@@ -133,6 +174,9 @@ class MyCustomLogger implements Logger {
export const entities = [
SkLatestNote,
+ SkApContext,
+ SkApFetchLog,
+ SkApInboxLog,
MiAnnouncement,
MiAnnouncementRead,
MiMeta,
@@ -249,7 +293,12 @@ export function createPostgresDataSource(config: Config) {
},
} : false,
logging: log,
- logger: log ? new MyCustomLogger() : undefined,
+ logger: log
+ ? new MyCustomLogger({
+ disableQueryTruncation: config.logging?.sql?.disableQueryTruncation,
+ enableQueryParamLogging: config.logging?.sql?.enableQueryParamLogging,
+ })
+ : undefined,
maxQueryExecutionTime: 300,
entities: entities,
migrations: ['../../migration/*.js'],
diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
index b81987cc15..ef21b6142e 100644
--- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -215,15 +215,10 @@ export class CheckModeratorsActivityProcessorService {
// -- SystemWebhook
- const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
- .then(it => it.filter(it => it.on.includes('inactiveModeratorsWarning')));
- for (const systemWebhook of systemWebhooks) {
- this.systemWebhookService.enqueueSystemWebhook(
- systemWebhook,
- 'inactiveModeratorsWarning',
- { remainingTime: remainingTime },
- );
- }
+ return this.systemWebhookService.enqueueSystemWebhook(
+ 'inactiveModeratorsWarning',
+ { remainingTime: remainingTime },
+ );
}
@bindThis
@@ -253,15 +248,10 @@ export class CheckModeratorsActivityProcessorService {
// -- SystemWebhook
- const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
- .then(it => it.filter(it => it.on.includes('inactiveModeratorsInvitationOnlyChanged')));
- for (const systemWebhook of systemWebhooks) {
- this.systemWebhookService.enqueueSystemWebhook(
- systemWebhook,
- 'inactiveModeratorsInvitationOnlyChanged',
- {},
- );
- }
+ return this.systemWebhookService.enqueueSystemWebhook(
+ 'inactiveModeratorsInvitationOnlyChanged',
+ {},
+ );
}
@bindThis
diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts
index 19f98c0d51..8c5faa8d07 100644
--- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts
@@ -48,6 +48,7 @@ export class CleanChartsProcessorService {
public async process(): Promise<void> {
this.logger.info('Clean charts...');
+ // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する
await this.federationChart.clean();
await this.notesChart.clean();
await this.usersChart.clean();
diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
index 0e604a0501..0c70829132 100644
--- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
@@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { DriveFilesRepository, NoteReactionsRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { DriveFilesRepository, NoteReactionsRepository, NotesRepository, UserProfilesRepository, UsersRepository, NoteScheduleRepository, MiNoteSchedule } from '@/models/_.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
@@ -15,10 +15,12 @@ import type { MiNoteReaction } from '@/models/NoteReaction.js';
import { EmailService } from '@/core/EmailService.js';
import { bindThis } from '@/decorators.js';
import { SearchService } from '@/core/SearchService.js';
+import { ApLogService } from '@/core/ApLogService.js';
+import { ReactionService } from '@/core/ReactionService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbUserDeleteJobData } from '../types.js';
-import { ReactionService } from '@/core/ReactionService.js';
+import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class DeleteAccountProcessorService {
@@ -40,11 +42,16 @@ export class DeleteAccountProcessorService {
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
+ @Inject(DI.noteScheduleRepository)
+ private noteScheduleRepository: NoteScheduleRepository,
+
+ private queueService: QueueService,
private driveService: DriveService,
private emailService: EmailService,
private queueLoggerService: QueueLoggerService,
private searchService: SearchService,
private reactionService: ReactionService,
+ private readonly apLogService: ApLogService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('delete-account');
}
@@ -58,6 +65,22 @@ export class DeleteAccountProcessorService {
return;
}
+ { // Delete scheduled notes
+ const scheduledNotes = await this.noteScheduleRepository.findBy({
+ userId: user.id,
+ }) as MiNoteSchedule[];
+
+ for (const note of scheduledNotes) {
+ await this.queueService.ScheduleNotePostQueue.remove(`schedNote:${note.id}`);
+ }
+
+ await this.noteScheduleRepository.delete({
+ userId: user.id,
+ });
+
+ this.logger.succ('All scheduled notes deleted');
+ }
+
{ // Delete notes
let cursor: MiNote['id'] | null = null;
@@ -84,6 +107,13 @@ export class DeleteAccountProcessorService {
for (const note of notes) {
await this.searchService.unindexNote(note);
}
+
+ // Delete note AP logs
+ const noteUris = notes.map(n => n.uri).filter(u => !!u) as string[];
+ if (noteUris.length > 0) {
+ await this.apLogService.deleteObjectLogs(noteUris)
+ .catch(err => this.logger.error(err, `Failed to delete AP logs for notes of user '${user.uri ?? user.id}'`));
+ }
}
this.logger.succ('All of notes deleted');
@@ -149,6 +179,13 @@ export class DeleteAccountProcessorService {
this.logger.succ('All of files deleted');
}
+ { // Delete actor logs
+ if (user.uri) {
+ await this.apLogService.deleteObjectLogs(user.uri)
+ .catch(err => this.logger.error(err, `Failed to delete AP logs for user '${user.uri}'`));
+ }
+ }
+
{ // Send email notification
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.email && profile.emailVerified) {
diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
index 17ba71df3d..383fa0c26a 100644
--- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
@@ -14,6 +14,7 @@ import { createTempDir } from '@/misc/create-temp.js';
import { DriveService } from '@/core/DriveService.js';
import { DownloadService } from '@/core/DownloadService.js';
import { bindThis } from '@/decorators.js';
+import type { Config } from '@/config.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbUserImportJobData } from '../types.js';
@@ -24,6 +25,9 @@ export class ImportCustomEmojisProcessorService {
private logger: Logger;
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@@ -57,7 +61,7 @@ export class ImportCustomEmojisProcessorService {
try {
fs.writeFileSync(destPath, '', 'binary');
- await this.downloadService.downloadUrl(file.url, destPath);
+ await this.downloadService.downloadUrl(file.url, destPath, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize });
} catch (e) { // TODO: 何度か再試行
if (e instanceof Error || typeof e === 'string') {
this.logger.error(e);
@@ -88,6 +92,7 @@ export class ImportCustomEmojisProcessorService {
await this.emojisRepository.delete({
name: nameNfc,
});
+
try {
const driveFile = await this.driveService.addFile({
user: null,
@@ -96,11 +101,13 @@ export class ImportCustomEmojisProcessorService {
force: true,
});
await this.customEmojiService.add({
+ originalUrl: driveFile.url,
+ publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+ fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: emojiInfo.category?.normalize('NFC'),
host: null,
aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')),
- driveFile,
license: emojiInfo.license,
isSensitive: emojiInfo.isSensitive,
localOnly: emojiInfo.localOnly,
diff --git a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts
index f89dc46722..ee9819b29f 100644
--- a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts
@@ -626,7 +626,7 @@ export class ImportNotesProcessorService {
if (!exists) {
try {
- await this.downloadService.downloadUrl(videos[0].url, filePath);
+ await this.downloadUrl(videos[0].url, filePath);
} catch (e) { // TODO: 何度か再試行
this.logger.error(e instanceof Error ? e : new Error(e as string));
}
@@ -651,7 +651,7 @@ export class ImportNotesProcessorService {
if (!exists) {
try {
- await this.downloadService.downloadUrl(file.media_url_https, filePath);
+ await this.downloadUrl(file.media_url_https, filePath);
} catch (e) { // TODO: 何度か再試行
this.logger.error(e instanceof Error ? e : new Error(e as string));
}
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 7727a3e985..35a0bf095d 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -29,6 +29,9 @@ import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { MiNote } from '@/models/Note.js';
import { MiMeta } from '@/models/Meta.js';
import { DI } from '@/di-symbols.js';
+import { SkApInboxLog } from '@/models/_.js';
+import type { Config } from '@/config.js';
+import { ApLogService, calculateDurationSince } from '@/core/ApLogService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { InboxJobData } from '../types.js';
@@ -46,6 +49,9 @@ export class InboxProcessorService implements OnApplicationShutdown {
@Inject(DI.meta)
private meta: MiMeta,
+ @Inject(DI.config)
+ private config: Config,
+
private utilityService: UtilityService,
private apInboxService: ApInboxService,
private federatedInstanceService: FederatedInstanceService,
@@ -57,6 +63,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
private apRequestChart: ApRequestChart,
private federationChart: FederationChart,
private queueLoggerService: QueueLoggerService,
+ private readonly apLogService: ApLogService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
@@ -64,6 +71,41 @@ export class InboxProcessorService implements OnApplicationShutdown {
@bindThis
public async process(job: Bull.Job<InboxJobData>): Promise<string> {
+ if (this.config.activityLogging.enabled) {
+ return await this._processLogged(job);
+ } else {
+ return await this._process(job);
+ }
+ }
+
+ private async _processLogged(job: Bull.Job<InboxJobData>): Promise<string> {
+ const startTime = process.hrtime.bigint();
+ const activity = job.data.activity;
+ const keyId = job.data.signature.keyId;
+ const log = await this.apLogService.createInboxLog({ activity, keyId });
+
+ try {
+ const result = await this._process(job, log);
+
+ log.accepted = result.startsWith('ok');
+ log.result = result;
+
+ return result;
+ } catch (err) {
+ log.accepted = false;
+ log.result = String(err);
+
+ throw err;
+ } finally {
+ log.duration = calculateDurationSince(startTime);
+
+ // Save or finalize asynchronously
+ this.apLogService.saveInboxLog(log)
+ .catch(err => this.logger.error('Failed to record AP activity:', err));
+ }
+ }
+
+ private async _process(job: Bull.Job<InboxJobData>, log?: SkApInboxLog): Promise<string> {
const signature = job.data.signature; // HTTP-signature
let activity = job.data.activity;
@@ -197,6 +239,13 @@ export class InboxProcessorService implements OnApplicationShutdown {
delete activity.id;
}
+ // Record verified user in log
+ if (log) {
+ log.verified = true;
+ log.authUser = authUser.user;
+ log.authUserId = authUser.user.id;
+ }
+
this.apRequestChart.inbox();
this.federationChart.inbox(authUser.user.host);
@@ -248,6 +297,14 @@ export class InboxProcessorService implements OnApplicationShutdown {
return `skip: permanent error ${e.statusCode}`;
}
+ if (e instanceof IdentifiableError && !e.isRetryable) {
+ if (e.message) {
+ return `skip: permanent error ${e.id}: ${e.message}`;
+ } else {
+ return `skip: permanent error ${e.id}`;
+ }
+ }
+
throw e;
}
return 'ok';
diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts
index 46e1adf173..0c47fdedb3 100644
--- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts
@@ -29,6 +29,7 @@ export class ResyncChartsProcessorService {
public async process(): Promise<void> {
this.logger.info('Resync charts...');
+ // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する
// TODO: ユーザーごとのチャートも更新する
// TODO: インスタンスごとのチャートも更新する
await this.driveChart.resync();
diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts
index c09cbccc57..fc8856a271 100644
--- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts
+++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts
@@ -48,6 +48,7 @@ export class TickChartsProcessorService {
public async process(): Promise<void> {
this.logger.info('Tick charts...');
+ // DBへの同時接続を避けるためにPromise.allを使わずひとつずつ実行する
await this.federationChart.tick(false);
await this.notesChart.tick(false);
await this.usersChart.tick(false);
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index 9433392df5..a900675a86 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -6,9 +6,12 @@
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
+import type { SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { MiUser } from '@/models/User.js';
-import type { MiWebhook } from '@/models/Webhook.js';
+import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import type { IActivity } from '@/core/activitypub/type.js';
+import type { SystemWebhookPayload } from '@/core/SystemWebhookService.js';
+import type { UserWebhookPayload } from '@/core/UserWebhookService.js';
import type httpSignature from '@peertube/http-signature';
export type DeliverJobData = {
@@ -131,9 +134,9 @@ export type EndedPollNotificationJobData = {
noteId: MiNote['id'];
};
-export type SystemWebhookDeliverJobData = {
- type: string;
- content: unknown;
+export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = {
+ type: T;
+ content: SystemWebhookPayload<T>;
webhookId: MiWebhook['id'];
to: string;
secret: string;
@@ -141,9 +144,9 @@ export type SystemWebhookDeliverJobData = {
eventId: string;
};
-export type UserWebhookDeliverJobData = {
- type: string;
- content: unknown;
+export type UserWebhookDeliverJobData<T extends WebhookEventTypes = WebhookEventTypes> = {
+ type: T;
+ content: UserWebhookPayload<T>;
webhookId: MiWebhook['id'];
userId: MiUser['id'];
to: string;
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index 815bf278c7..10dba1660f 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -30,14 +30,14 @@ import type { MiNote } from '@/models/Note.js';
import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import type Logger from '@/logger.js';
+import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
-import type Logger from '@/logger.js';
-import { LoggerService } from '@/core/LoggerService.js';
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
@@ -103,15 +103,16 @@ export class ActivityPubServerService {
/**
* Pack Create<Note> or Announce Activity
* @param note Note
+ * @param author Author of the note
*/
@bindThis
- private async packActivity(note: MiNote): Promise<any> {
+ private async packActivity(note: MiNote, author: MiUser): Promise<any> {
if (isRenote(note) && !isQuote(note)) {
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
}
- return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
+ return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author, false), note);
}
@bindThis
@@ -506,7 +507,7 @@ export class ActivityPubServerService {
this.notesRepository.findOneByOrFail({ id: pining.noteId }))))
.filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility));
- const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note)));
+ const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note, user)));
const rendered = this.apRendererService.renderOrderedCollection(
`${this.config.url}/users/${userId}/collections/featured`,
@@ -579,7 +580,7 @@ export class ActivityPubServerService {
if (sinceId) notes.reverse();
- const activities = await Promise.all(notes.map(note => this.packActivity(note)));
+ const activities = await Promise.all(notes.map(note => this.packActivity(note, user)));
const rendered = this.apRendererService.renderOrderedCollectionPage(
`${partOf}?${url.query({
page: 'true',
@@ -653,8 +654,8 @@ export class ActivityPubServerService {
},
deriveConstraint(request: IncomingMessage) {
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
- const isAp = typeof accepted === 'string' && !accepted.match(/html/);
- return isAp ? 'ap' : 'html';
+ if (accepted === false) return null;
+ return accepted !== 'html' ? 'ap' : 'html';
},
});
@@ -723,7 +724,9 @@ export class ActivityPubServerService {
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
- return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false));
+
+ const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
+ return this.apRendererService.addContext(await this.apRendererService.renderNote(note, author, false));
});
// note activity
@@ -746,7 +749,9 @@ export class ActivityPubServerService {
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
- return (this.apRendererService.addContext(await this.packActivity(note)));
+
+ const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
+ return (this.apRendererService.addContext(await this.packActivity(note, author)));
});
// outbox
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 5293d529ad..a7e13a1b78 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -11,7 +11,7 @@ import rename from 'rename';
import sharp from 'sharp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import type { Config } from '@/config.js';
-import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
+import type { MiDriveFile, DriveFilesRepository, MiUser } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { createTemp } from '@/misc/create-temp.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
@@ -30,8 +30,7 @@ import { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
-import { RoleService } from '@/core/RoleService.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { Keyed, RateLimit, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
@@ -59,7 +58,6 @@ export class FileServerService {
private loggerService: LoggerService,
private authenticateService: AuthenticateService,
private rateLimiterService: SkRateLimiterService,
- private roleService: RoleService,
) {
this.logger = this.loggerService.getLogger('server', 'gray');
@@ -625,14 +623,13 @@ export class FileServerService {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
const [user] = await this.authenticateService.authenticate(token);
- const actor = user?.id ?? getIpHash(request.ip);
- const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
+ const actor = user ?? getIpHash(request.ip);
// Call both limits: the per-resource limit and the shared cross-resource limit
- return await this.checkResourceLimit(reply, actor, group, resource, factor) && await this.checkSharedLimit(reply, actor, group, factor);
+ return await this.checkResourceLimit(reply, actor, group, resource) && await this.checkSharedLimit(reply, actor, group);
}
- private async checkResourceLimit(reply: FastifyReply, actor: string, group: string, resource: string, factor = 1): Promise<boolean> {
+ private async checkResourceLimit(reply: FastifyReply, actor: string | MiUser, group: string, resource: string): Promise<boolean> {
const limit: Keyed<RateLimit> = {
// Group by resource
key: `${group}${resource}`,
@@ -643,10 +640,10 @@ export class FileServerService {
dripRate: 1000 * 60,
};
- return await this.checkLimit(reply, actor, limit, factor);
+ return await this.checkLimit(reply, actor, limit);
}
- private async checkSharedLimit(reply: FastifyReply, actor: string, group: string, factor = 1): Promise<boolean> {
+ private async checkSharedLimit(reply: FastifyReply, actor: string | MiUser, group: string): Promise<boolean> {
const limit: Keyed<RateLimit> = {
key: group,
type: 'bucket',
@@ -655,11 +652,11 @@ export class FileServerService {
size: 3600,
};
- return await this.checkLimit(reply, actor, limit, factor);
+ return await this.checkLimit(reply, actor, limit);
}
- private async checkLimit(reply: FastifyReply, actor: string, limit: Keyed<RateLimit>, factor = 1): Promise<boolean> {
- const info = await this.rateLimiterService.limit(limit, actor, factor);
+ private async checkLimit(reply: FastifyReply, actor: string | MiUser, limit: Keyed<RateLimit>): Promise<boolean> {
+ const info = await this.rateLimiterService.limit(limit, actor);
sendRateLimitHeaders(reply, info);
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index c1d7c088f1..2c067afe88 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -6,7 +6,7 @@
import { Module } from '@nestjs/common';
import { EndpointsModule } from '@/server/api/EndpointsModule.js';
import { CoreModule } from '@/core/CoreModule.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js';
import { HealthServerService } from './HealthServerService.js';
@@ -27,6 +27,8 @@ import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { MastoConverters } from './api/mastodon/converters.js';
+import { MastodonLogger } from './api/mastodon/MastodonLogger.js';
+import { MastodonDataService } from './api/mastodon/MastodonDataService.js';
import { FeedService } from './web/FeedService.js';
import { UrlPreviewService } from './web/UrlPreviewService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
@@ -103,6 +105,8 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
MastodonApiServerService,
OAuth2ProviderService,
MastoConverters,
+ MastodonLogger,
+ MastodonDataService,
],
exports: [
ServerService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 43a2a3a2b0..690fdcfe29 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -262,7 +262,7 @@ export class ServerService implements OnApplicationShutdown {
}
});
} else {
- fastify.listen({ port: this.config.port, host: '0.0.0.0' });
+ fastify.listen({ port: this.config.port, host: this.config.address });
}
await fastify.ready();
diff --git a/packages/backend/src/server/SkRateLimiterService.md b/packages/backend/src/server/SkRateLimiterService.md
index 762f8dfe14..c8a2b4e85c 100644
--- a/packages/backend/src/server/SkRateLimiterService.md
+++ b/packages/backend/src/server/SkRateLimiterService.md
@@ -12,6 +12,11 @@ SkRateLimiterService is not quite plug-and-play compatible with existing call si
Instead, the returned LimitInfo object will have `blocked` set to true.
Callers are responsible for checking this property and taking any desired action, such as rejecting a request or returning limit details.
+Rate limit factors are also handled differently.
+Instead of providing an optional parameter for callers, SkRateLimiterServer accepts an `MiUser` parameter that is used to compute the factor directly.
+If a user is not available (such as for unauthenticated callers), then the Role Template factor is used instead.
+To avoid confusion, the `factor` parameter has been removed entirely and is now an implementation detail.
+
## Headers
LimitInfo objects (returned by `SkRateLimitService.limit()`) can be passed to `rate-limit-utils.sendRateLimitHeaders()` to send standard rate limit headers with an HTTP response.
@@ -34,6 +39,7 @@ The first call is read-only, while the others perform at least one write operati
Two integer keys are stored per client/subject, and both expire together after the maximum duration of the limit.
While performance has not been formally tested, it's expected that SkRateLimiterService has an impact roughly on par with the legacy RateLimiterService.
Redis memory usage should be notably lower due to the reduced number of keys and avoidance of set / array constructions.
+If redis load does become a concern, then a dedicated node can be assigned via the `redisForRateLimit` config setting.
## Concurrency and Multi-Node Correctness
diff --git a/packages/backend/src/server/api/SkRateLimiterService.ts b/packages/backend/src/server/SkRateLimiterService.ts
index 38c97b63df..30bf092e4f 100644
--- a/packages/backend/src/server/api/SkRateLimiterService.ts
+++ b/packages/backend/src/server/SkRateLimiterService.ts
@@ -5,36 +5,67 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
-import { TimeService } from '@/core/TimeService.js';
-import { EnvService } from '@/core/EnvService.js';
+import type { TimeService } from '@/core/TimeService.js';
+import type { EnvService } from '@/core/EnvService.js';
import { BucketRateLimit, LegacyRateLimit, LimitInfo, RateLimit, hasMinLimit, isLegacyRateLimit, Keyed, hasMaxLimit, disabledLimitInfo, MaxLegacyLimit, MinLegacyLimit } from '@/misc/rate-limit-utils.js';
import { DI } from '@/di-symbols.js';
+import { MemoryKVCache } from '@/misc/cache.js';
+import type { MiUser } from '@/models/_.js';
+import type { RoleService } from '@/core/RoleService.js';
+
+// Sentinel value used for caching the default role template.
+// Required because MemoryKVCache doesn't support null keys.
+const defaultUserKey = '';
@Injectable()
export class SkRateLimiterService {
+ // 1-minute cache interval
+ private readonly factorCache = new MemoryKVCache<number>(1000 * 60);
private readonly disabled: boolean;
constructor(
- @Inject(TimeService)
+ @Inject('TimeService')
private readonly timeService: TimeService,
- @Inject(DI.redis)
+ @Inject(DI.redisForRateLimit)
private readonly redisClient: Redis.Redis,
- @Inject(EnvService)
+ @Inject('RoleService')
+ private readonly roleService: RoleService,
+
+ @Inject('EnvService')
envService: EnvService,
) {
this.disabled = envService.env.NODE_ENV === 'test';
}
/**
- * Check & increment a rate limit
+ * Check & increment a rate limit for a client.
+ *
+ * If the client (actorOrUser) is passed as a string, then it uses the default rate limit factor from the role template.
+ * If the client (actorOrUser) is passed as an MiUser, then it queries the user's actual rate limit factor from their assigned roles.
+ *
+ * A factor of zero (0) will disable the limit, while any negative number will produce an error.
+ * A factor between zero (0) and one (1) will increase the limit from its default values (allowing more actions per time interval).
+ * A factor greater than one (1) will decrease the limit from its default values (allowing fewer actions per time interval).
+ *
* @param limit The limit definition
- * @param actor Client who is calling this limit
- * @param factor Scaling factor - smaller = larger limit (less restrictive)
+ * @param actorOrUser authenticated client user or IP hash
*/
- public async limit(limit: Keyed<RateLimit>, actor: string, factor = 1): Promise<LimitInfo> {
- if (this.disabled || factor === 0) {
+ public async limit(limit: Keyed<RateLimit>, actorOrUser: string | MiUser): Promise<LimitInfo> {
+ if (this.disabled) {
+ return disabledLimitInfo;
+ }
+
+ const actor = typeof(actorOrUser) === 'object' ? actorOrUser.id : actorOrUser;
+ const userCacheKey = typeof(actorOrUser) === 'object' ? actorOrUser.id : defaultUserKey;
+ const userRoleKey = typeof(actorOrUser) === 'object' ? actorOrUser.id : null;
+ const factor = this.factorCache.get(userCacheKey) ?? await this.factorCache.fetch(userCacheKey, async () => {
+ const role = await this.roleService.getUserPolicies(userRoleKey);
+ return role.rateLimitFactor;
+ });
+
+ if (factor === 0) {
return disabledLimitInfo;
}
@@ -42,10 +73,6 @@ export class SkRateLimiterService {
throw new Error(`Rate limit factor is zero or negative: ${factor}`);
}
- return await this.tryLimit(limit, actor, factor);
- }
-
- private async tryLimit(limit: Keyed<RateLimit>, actor: string, factor: number): Promise<LimitInfo> {
if (isLegacyRateLimit(limit)) {
return await this.limitLegacy(limit, actor, factor);
} else {
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 03f25a51fe..5ce358d68f 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -19,7 +19,7 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { Config } from '@/config.js';
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { ApiError } from './error.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
@@ -313,35 +313,30 @@ export class ApiCallService implements OnApplicationShutdown {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (endpointLimit) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
- let limitActor: string;
+ let limitActor: string | MiLocalUser;
if (user) {
- limitActor = user.id;
+ limitActor = user;
} else {
limitActor = getIpHash(request.ip);
}
- // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
- const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
+ const limit = {
+ key: ep.name,
+ ...endpointLimit,
+ };
- if (factor > 0) {
- const limit = {
- key: ep.name,
- ...endpointLimit,
- };
+ // Rate limit
+ const info = await this.rateLimiterService.limit(limit, limitActor);
- // Rate limit
- const info = await this.rateLimiterService.limit(limit, limitActor, factor);
+ sendRateLimitHeaders(reply, info);
- sendRateLimitHeaders(reply, info);
-
- if (info.blocked) {
- throw new ApiError({
- message: 'Rate limit exceeded. Please try again later.',
- code: 'RATE_LIMIT_EXCEEDED',
- id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
- httpStatusCode: 429,
- }, info);
- }
+ if (info.blocked) {
+ throw new ApiError({
+ message: 'Rate limit exceeded. Please try again later.',
+ code: 'RATE_LIMIT_EXCEEDED',
+ id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+ httpStatusCode: 429,
+ }, info);
}
}
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index e319d6e0a4..9cfb2f0ac0 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -6,816 +6,13 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
-import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
-import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
-import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
-import * as ep___admin_abuseReport_notificationRecipient_update from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
-import * as ep___admin_abuseReport_notificationRecipient_delete from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
-import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
-import * as ep___admin_meta from './endpoints/admin/meta.js';
-import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
-import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
-import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
-import * as ep___admin_ad_create from './endpoints/admin/ad/create.js';
-import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js';
-import * as ep___admin_ad_list from './endpoints/admin/ad/list.js';
-import * as ep___admin_ad_update from './endpoints/admin/ad/update.js';
-import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js';
-import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
-import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
-import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
-import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
-import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
-import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
-import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
-import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
-import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
-import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
-import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
-import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
-import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
-import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js';
-import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js';
-import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js';
-import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js';
-import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js';
-import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
-import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js';
-import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js';
-import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
-import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
-import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
-import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
-import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
-import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
-import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
-import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
-import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
-import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
-import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
-import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
-import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
-import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
-import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
-import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
-import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
-import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
-import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js';
-import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js';
-import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js';
-import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
-import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
-import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
-import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
-import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
-import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
-import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
-import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
-import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
-import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
-import * as ep___admin_showUser from './endpoints/admin/show-user.js';
-import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
-import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js';
-import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
-import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
-import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
-import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
-import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
-import * as ep___admin_declineUser from './endpoints/admin/decline-user.js';
-import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
-import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
-import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
-import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
-import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
-import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
-import * as ep___admin_roles_list from './endpoints/admin/roles/list.js';
-import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
-import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
-import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
-import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
-import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
-import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
-import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
-import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
-import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
-import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
-import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
-import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
-import * as ep___announcements from './endpoints/announcements.js';
-import * as ep___announcements_show from './endpoints/announcements/show.js';
-import * as ep___antennas_create from './endpoints/antennas/create.js';
-import * as ep___antennas_delete from './endpoints/antennas/delete.js';
-import * as ep___antennas_list from './endpoints/antennas/list.js';
-import * as ep___antennas_notes from './endpoints/antennas/notes.js';
-import * as ep___antennas_show from './endpoints/antennas/show.js';
-import * as ep___antennas_update from './endpoints/antennas/update.js';
-import * as ep___ap_get from './endpoints/ap/get.js';
-import * as ep___ap_show from './endpoints/ap/show.js';
-import * as ep___app_create from './endpoints/app/create.js';
-import * as ep___app_show from './endpoints/app/show.js';
-import * as ep___auth_accept from './endpoints/auth/accept.js';
-import * as ep___auth_session_generate from './endpoints/auth/session/generate.js';
-import * as ep___auth_session_show from './endpoints/auth/session/show.js';
-import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js';
-import * as ep___blocking_create from './endpoints/blocking/create.js';
-import * as ep___blocking_delete from './endpoints/blocking/delete.js';
-import * as ep___blocking_list from './endpoints/blocking/list.js';
-import * as ep___channels_create from './endpoints/channels/create.js';
-import * as ep___channels_featured from './endpoints/channels/featured.js';
-import * as ep___channels_follow from './endpoints/channels/follow.js';
-import * as ep___channels_followed from './endpoints/channels/followed.js';
-import * as ep___channels_owned from './endpoints/channels/owned.js';
-import * as ep___channels_show from './endpoints/channels/show.js';
-import * as ep___channels_timeline from './endpoints/channels/timeline.js';
-import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
-import * as ep___channels_update from './endpoints/channels/update.js';
-import * as ep___channels_favorite from './endpoints/channels/favorite.js';
-import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
-import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
-import * as ep___channels_search from './endpoints/channels/search.js';
-import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
-import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
-import * as ep___charts_drive from './endpoints/charts/drive.js';
-import * as ep___charts_federation from './endpoints/charts/federation.js';
-import * as ep___charts_instance from './endpoints/charts/instance.js';
-import * as ep___charts_notes from './endpoints/charts/notes.js';
-import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
-import * as ep___charts_user_following from './endpoints/charts/user/following.js';
-import * as ep___charts_user_notes from './endpoints/charts/user/notes.js';
-import * as ep___charts_user_pv from './endpoints/charts/user/pv.js';
-import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js';
-import * as ep___charts_users from './endpoints/charts/users.js';
-import * as ep___clips_addNote from './endpoints/clips/add-note.js';
-import * as ep___clips_removeNote from './endpoints/clips/remove-note.js';
-import * as ep___clips_create from './endpoints/clips/create.js';
-import * as ep___clips_delete from './endpoints/clips/delete.js';
-import * as ep___clips_list from './endpoints/clips/list.js';
-import * as ep___clips_notes from './endpoints/clips/notes.js';
-import * as ep___clips_show from './endpoints/clips/show.js';
-import * as ep___clips_update from './endpoints/clips/update.js';
-import * as ep___clips_favorite from './endpoints/clips/favorite.js';
-import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
-import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
-import * as ep___drive from './endpoints/drive.js';
-import * as ep___drive_files from './endpoints/drive/files.js';
-import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
-import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js';
-import * as ep___drive_files_create from './endpoints/drive/files/create.js';
-import * as ep___drive_files_delete from './endpoints/drive/files/delete.js';
-import * as ep___drive_files_findByHash from './endpoints/drive/files/find-by-hash.js';
-import * as ep___drive_files_find from './endpoints/drive/files/find.js';
-import * as ep___drive_files_show from './endpoints/drive/files/show.js';
-import * as ep___drive_files_update from './endpoints/drive/files/update.js';
-import * as ep___drive_files_uploadFromUrl from './endpoints/drive/files/upload-from-url.js';
-import * as ep___drive_folders from './endpoints/drive/folders.js';
-import * as ep___drive_folders_create from './endpoints/drive/folders/create.js';
-import * as ep___drive_folders_delete from './endpoints/drive/folders/delete.js';
-import * as ep___drive_folders_find from './endpoints/drive/folders/find.js';
-import * as ep___drive_folders_show from './endpoints/drive/folders/show.js';
-import * as ep___drive_folders_update from './endpoints/drive/folders/update.js';
-import * as ep___drive_stream from './endpoints/drive/stream.js';
-import * as ep___emailAddress_available from './endpoints/email-address/available.js';
-import * as ep___endpoint from './endpoints/endpoint.js';
-import * as ep___endpoints from './endpoints/endpoints.js';
-import * as ep___exportCustomEmojis from './endpoints/export-custom-emojis.js';
-import * as ep___federation_followers from './endpoints/federation/followers.js';
-import * as ep___federation_following from './endpoints/federation/following.js';
-import * as ep___federation_instances from './endpoints/federation/instances.js';
-import * as ep___federation_showInstance from './endpoints/federation/show-instance.js';
-import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js';
-import * as ep___federation_users from './endpoints/federation/users.js';
-import * as ep___federation_stats from './endpoints/federation/stats.js';
-import * as ep___following_create from './endpoints/following/create.js';
-import * as ep___following_delete from './endpoints/following/delete.js';
-import * as ep___following_update from './endpoints/following/update.js';
-import * as ep___following_update_all from './endpoints/following/update-all.js';
-import * as ep___following_invalidate from './endpoints/following/invalidate.js';
-import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
-import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
-import * as ep___following_requests_list from './endpoints/following/requests/list.js';
-import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
-import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
-import * as ep___gallery_featured from './endpoints/gallery/featured.js';
-import * as ep___gallery_popular from './endpoints/gallery/popular.js';
-import * as ep___gallery_posts from './endpoints/gallery/posts.js';
-import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js';
-import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js';
-import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js';
-import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
-import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
-import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
-import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
-import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
-import * as ep___hashtags_list from './endpoints/hashtags/list.js';
-import * as ep___hashtags_search from './endpoints/hashtags/search.js';
-import * as ep___hashtags_show from './endpoints/hashtags/show.js';
-import * as ep___hashtags_trend from './endpoints/hashtags/trend.js';
-import * as ep___hashtags_users from './endpoints/hashtags/users.js';
-import * as ep___i from './endpoints/i.js';
-import * as ep___i_2fa_done from './endpoints/i/2fa/done.js';
-import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
-import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
-import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
-import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
-import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
-import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
-import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
-import * as ep___i_apps from './endpoints/i/apps.js';
-import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
-import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
-import * as ep___i_changePassword from './endpoints/i/change-password.js';
-import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
-import * as ep___i_exportData from './endpoints/i/export-data.js';
-import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
-import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
-import * as ep___i_exportMute from './endpoints/i/export-mute.js';
-import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
-import * as ep___i_exportClips from './endpoints/i/export-clips.js';
-import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
-import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
-import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
-import * as ep___i_favorites from './endpoints/i/favorites.js';
-import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
-import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
-import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
-import * as ep___i_importFollowing from './endpoints/i/import-following.js';
-import * as ep___i_importNotes from './endpoints/i/import-notes.js';
-import * as ep___i_importMuting from './endpoints/i/import-muting.js';
-import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
-import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
-import * as ep___i_notifications from './endpoints/i/notifications.js';
-import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js';
-import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
-import * as ep___i_pages from './endpoints/i/pages.js';
-import * as ep___i_pin from './endpoints/i/pin.js';
-import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js';
-import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js';
-import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js';
-import * as ep___i_registry_getAll from './endpoints/i/registry/get-all.js';
-import * as ep___i_registry_getUnsecure from './endpoints/i/registry/get-unsecure.js';
-import * as ep___i_registry_getDetail from './endpoints/i/registry/get-detail.js';
-import * as ep___i_registry_get from './endpoints/i/registry/get.js';
-import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
-import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
-import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
-import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
-import * as ep___i_registry_set from './endpoints/i/registry/set.js';
-import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
-import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
-import * as ep___i_unpin from './endpoints/i/unpin.js';
-import * as ep___i_updateEmail from './endpoints/i/update-email.js';
-import * as ep___i_update from './endpoints/i/update.js';
-import * as ep___i_move from './endpoints/i/move.js';
-import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
-import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
-import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
-import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
-import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
-import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
-import * as ep___invite_create from './endpoints/invite/create.js';
-import * as ep___invite_delete from './endpoints/invite/delete.js';
-import * as ep___invite_list from './endpoints/invite/list.js';
-import * as ep___invite_limit from './endpoints/invite/limit.js';
-import * as ep___meta from './endpoints/meta.js';
-import * as ep___emojis from './endpoints/emojis.js';
-import * as ep___emoji from './endpoints/emoji.js';
-import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
-import * as ep___mute_create from './endpoints/mute/create.js';
-import * as ep___mute_delete from './endpoints/mute/delete.js';
-import * as ep___mute_list from './endpoints/mute/list.js';
-import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
-import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
-import * as ep___renoteMute_list from './endpoints/renote-mute/list.js';
-import * as ep___my_apps from './endpoints/my/apps.js';
-import * as ep___notes from './endpoints/notes.js';
-import * as ep___notes_children from './endpoints/notes/children.js';
-import * as ep___notes_clips from './endpoints/notes/clips.js';
-import * as ep___notes_conversation from './endpoints/notes/conversation.js';
-import * as ep___notes_create from './endpoints/notes/create.js';
-import * as ep___notes_delete from './endpoints/notes/delete.js';
-import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
-import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
-import * as ep___notes_featured from './endpoints/notes/featured.js';
-import * as ep___notes_following from './endpoints/notes/following.js';
-import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
-import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js';
-import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
-import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
-import * as ep___notes_mentions from './endpoints/notes/mentions.js';
-import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
-import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
-import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js';
-import * as ep___notes_reactions from './endpoints/notes/reactions.js';
-import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js';
-import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js';
-import * as ep___notes_like from './endpoints/notes/like.js';
-import * as ep___notes_renotes from './endpoints/notes/renotes.js';
-import * as ep___notes_replies from './endpoints/notes/replies.js';
-import * as ep___notes_edit from './endpoints/notes/edit.js';
-import * as ep___notes_versions from './endpoints/notes/versions.js';
-import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js';
-import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
-import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js';
-import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js';
-import * as ep___notes_search from './endpoints/notes/search.js';
-import * as ep___notes_show from './endpoints/notes/show.js';
-import * as ep___notes_state from './endpoints/notes/state.js';
-import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js';
-import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js';
-import * as ep___notes_timeline from './endpoints/notes/timeline.js';
-import * as ep___notes_translate from './endpoints/notes/translate.js';
-import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
-import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
-import * as ep___notifications_create from './endpoints/notifications/create.js';
-import * as ep___notifications_flush from './endpoints/notifications/flush.js';
-import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
-import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
-import * as ep___pagePush from './endpoints/page-push.js';
-import * as ep___pages_create from './endpoints/pages/create.js';
-import * as ep___pages_delete from './endpoints/pages/delete.js';
-import * as ep___pages_featured from './endpoints/pages/featured.js';
-import * as ep___pages_like from './endpoints/pages/like.js';
-import * as ep___pages_show from './endpoints/pages/show.js';
-import * as ep___pages_unlike from './endpoints/pages/unlike.js';
-import * as ep___pages_update from './endpoints/pages/update.js';
-import * as ep___flash_create from './endpoints/flash/create.js';
-import * as ep___flash_delete from './endpoints/flash/delete.js';
-import * as ep___flash_featured from './endpoints/flash/featured.js';
-import * as ep___flash_like from './endpoints/flash/like.js';
-import * as ep___flash_show from './endpoints/flash/show.js';
-import * as ep___flash_unlike from './endpoints/flash/unlike.js';
-import * as ep___flash_update from './endpoints/flash/update.js';
-import * as ep___flash_my from './endpoints/flash/my.js';
-import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
-import * as ep___ping from './endpoints/ping.js';
-import * as ep___pinnedUsers from './endpoints/pinned-users.js';
-import * as ep___promo_read from './endpoints/promo/read.js';
-import * as ep___roles_list from './endpoints/roles/list.js';
-import * as ep___roles_show from './endpoints/roles/show.js';
-import * as ep___roles_users from './endpoints/roles/users.js';
-import * as ep___roles_notes from './endpoints/roles/notes.js';
-import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
-import * as ep___resetDb from './endpoints/reset-db.js';
-import * as ep___resetPassword from './endpoints/reset-password.js';
-import * as ep___serverInfo from './endpoints/server-info.js';
-import * as ep___stats from './endpoints/stats.js';
-import * as ep___sw_show_registration from './endpoints/sw/show-registration.js';
-import * as ep___sw_update_registration from './endpoints/sw/update-registration.js';
-import * as ep___sw_register from './endpoints/sw/register.js';
-import * as ep___sw_unregister from './endpoints/sw/unregister.js';
-import * as ep___test from './endpoints/test.js';
-import * as ep___username_available from './endpoints/username/available.js';
-import * as ep___users from './endpoints/users.js';
-import * as ep___users_clips from './endpoints/users/clips.js';
-import * as ep___users_followers from './endpoints/users/followers.js';
-import * as ep___users_following from './endpoints/users/following.js';
-import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
-import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
-import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
-import * as ep___users_lists_create from './endpoints/users/lists/create.js';
-import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
-import * as ep___users_lists_list from './endpoints/users/lists/list.js';
-import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
-import * as ep___users_lists_push from './endpoints/users/lists/push.js';
-import * as ep___users_lists_show from './endpoints/users/lists/show.js';
-import * as ep___users_lists_update from './endpoints/users/lists/update.js';
-import * as ep___users_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_createFromPublic from './endpoints/users/lists/create-from-public.js';
-import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
-import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.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_flashs from './endpoints/users/flashs.js';
-import * as ep___users_reactions from './endpoints/users/reactions.js';
-import * as ep___users_recommendation from './endpoints/users/recommendation.js';
-import * as ep___users_relation from './endpoints/users/relation.js';
-import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
-import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
-import * as ep___users_search from './endpoints/users/search.js';
-import * as ep___users_show from './endpoints/users/show.js';
-import * as ep___users_achievements from './endpoints/users/achievements.js';
-import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
-import * as ep___fetchRss from './endpoints/fetch-rss.js';
-import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
-import * as ep___retention from './endpoints/retention.js';
-import * as ep___sponsors from './endpoints/sponsors.js';
-import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
-import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
-import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js';
-import * as ep___reversi_games from './endpoints/reversi/games.js';
-import * as ep___reversi_match from './endpoints/reversi/match.js';
-import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
-import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
-import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
-import * as ep___reversi_verify from './endpoints/reversi/verify.js';
+import * as endpointsObject from './endpoint-list.js';
import { GetterService } from './GetterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import type { Provider } from '@nestjs/common';
-const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
-const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
-const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default };
-const $admin_abuseReport_notificationRecipient_show: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/show', useClass: ep___admin_abuseReport_notificationRecipient_show.default };
-const $admin_abuseReport_notificationRecipient_create: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/create', useClass: ep___admin_abuseReport_notificationRecipient_create.default };
-const $admin_abuseReport_notificationRecipient_update: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/update', useClass: ep___admin_abuseReport_notificationRecipient_update.default };
-const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/delete', useClass: ep___admin_abuseReport_notificationRecipient_delete.default };
-const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
-const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
-const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default };
-const $admin_ad_create: Provider = { provide: 'ep:admin/ad/create', useClass: ep___admin_ad_create.default };
-const $admin_ad_delete: Provider = { provide: 'ep:admin/ad/delete', useClass: ep___admin_ad_delete.default };
-const $admin_ad_list: Provider = { provide: 'ep:admin/ad/list', useClass: ep___admin_ad_list.default };
-const $admin_ad_update: Provider = { provide: 'ep:admin/ad/update', useClass: ep___admin_ad_update.default };
-const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements/create', useClass: ep___admin_announcements_create.default };
-const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default };
-const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default };
-const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default };
-const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-decorations/create', useClass: ep___admin_avatarDecorations_create.default };
-const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default };
-const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
-const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
-const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
-const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
-const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
-const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
-const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
-const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
-const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default };
-const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default };
-const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.default };
-const $admin_emoji_copy: Provider = { provide: 'ep:admin/emoji/copy', useClass: ep___admin_emoji_copy.default };
-const $admin_emoji_deleteBulk: Provider = { provide: 'ep:admin/emoji/delete-bulk', useClass: ep___admin_emoji_deleteBulk.default };
-const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default };
-const $admin_emoji_importZip: Provider = { provide: 'ep:admin/emoji/import-zip', useClass: ep___admin_emoji_importZip.default };
-const $admin_emoji_listRemote: Provider = { provide: 'ep:admin/emoji/list-remote', useClass: ep___admin_emoji_listRemote.default };
-const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass: ep___admin_emoji_list.default };
-const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default };
-const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default };
-const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default };
-const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default };
-const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default };
-const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default };
-const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
-const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default };
-const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federation/update-instance', useClass: ep___admin_federation_updateInstance.default };
-const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
-const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
-const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
-const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
-const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
-const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
-const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
-const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
-const $admin_queue_inboxDelayed: Provider = { provide: 'ep:admin/queue/inbox-delayed', useClass: ep___admin_queue_inboxDelayed.default };
-const $admin_queue_promote: Provider = { provide: 'ep:admin/queue/promote', useClass: ep___admin_queue_promote.default };
-const $admin_queue_stats: Provider = { provide: 'ep:admin/queue/stats', useClass: ep___admin_queue_stats.default };
-const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: ep___admin_relays_add.default };
-const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default };
-const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
-const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
-const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
-const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
-const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
-const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
-const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
-const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
-const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default };
-const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default };
-const $admin_nsfwUser: Provider = { provide: 'ep:admin/nsfw-user', useClass: ep___admin_nsfwUser.default };
-const $admin_unnsfwUser: Provider = { provide: 'ep:admin/unnsfw-user', useClass: ep___admin_unnsfwUser.default };
-const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClass: ep___admin_silenceUser.default };
-const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default };
-const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
-const $admin_approveUser: Provider = { provide: 'ep:admin/approve-user', useClass: ep___admin_approveUser.default };
-const $admin_declineUser: Provider = { provide: 'ep:admin/decline-user', useClass: ep___admin_declineUser.default };
-const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
-const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
-const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default };
-const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default };
-const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default };
-const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default };
-const $admin_roles_list: Provider = { provide: 'ep:admin/roles/list', useClass: ep___admin_roles_list.default };
-const $admin_roles_show: Provider = { provide: 'ep:admin/roles/show', useClass: ep___admin_roles_show.default };
-const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default };
-const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default };
-const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
-const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
-const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
-const $admin_systemWebhook_create: Provider = { provide: 'ep:admin/system-webhook/create', useClass: ep___admin_systemWebhook_create.default };
-const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhook/delete', useClass: ep___admin_systemWebhook_delete.default };
-const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
-const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
-const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
-const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
-const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
-const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
-const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
-const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
-const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default };
-const $antennas_notes: Provider = { provide: 'ep:antennas/notes', useClass: ep___antennas_notes.default };
-const $antennas_show: Provider = { provide: 'ep:antennas/show', useClass: ep___antennas_show.default };
-const $antennas_update: Provider = { provide: 'ep:antennas/update', useClass: ep___antennas_update.default };
-const $ap_get: Provider = { provide: 'ep:ap/get', useClass: ep___ap_get.default };
-const $ap_show: Provider = { provide: 'ep:ap/show', useClass: ep___ap_show.default };
-const $app_create: Provider = { provide: 'ep:app/create', useClass: ep___app_create.default };
-const $app_show: Provider = { provide: 'ep:app/show', useClass: ep___app_show.default };
-const $auth_accept: Provider = { provide: 'ep:auth/accept', useClass: ep___auth_accept.default };
-const $auth_session_generate: Provider = { provide: 'ep:auth/session/generate', useClass: ep___auth_session_generate.default };
-const $auth_session_show: Provider = { provide: 'ep:auth/session/show', useClass: ep___auth_session_show.default };
-const $auth_session_userkey: Provider = { provide: 'ep:auth/session/userkey', useClass: ep___auth_session_userkey.default };
-const $blocking_create: Provider = { provide: 'ep:blocking/create', useClass: ep___blocking_create.default };
-const $blocking_delete: Provider = { provide: 'ep:blocking/delete', useClass: ep___blocking_delete.default };
-const $blocking_list: Provider = { provide: 'ep:blocking/list', useClass: ep___blocking_list.default };
-const $channels_create: Provider = { provide: 'ep:channels/create', useClass: ep___channels_create.default };
-const $channels_featured: Provider = { provide: 'ep:channels/featured', useClass: ep___channels_featured.default };
-const $channels_follow: Provider = { provide: 'ep:channels/follow', useClass: ep___channels_follow.default };
-const $channels_followed: Provider = { provide: 'ep:channels/followed', useClass: ep___channels_followed.default };
-const $channels_owned: Provider = { provide: 'ep:channels/owned', useClass: ep___channels_owned.default };
-const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___channels_show.default };
-const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default };
-const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default };
-const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default };
-const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default };
-const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default };
-const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default };
-const $channels_search: Provider = { provide: 'ep:channels/search', useClass: ep___channels_search.default };
-const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
-const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
-const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
-const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default };
-const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default };
-const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default };
-const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default };
-const $charts_user_following: Provider = { provide: 'ep:charts/user/following', useClass: ep___charts_user_following.default };
-const $charts_user_notes: Provider = { provide: 'ep:charts/user/notes', useClass: ep___charts_user_notes.default };
-const $charts_user_pv: Provider = { provide: 'ep:charts/user/pv', useClass: ep___charts_user_pv.default };
-const $charts_user_reactions: Provider = { provide: 'ep:charts/user/reactions', useClass: ep___charts_user_reactions.default };
-const $charts_users: Provider = { provide: 'ep:charts/users', useClass: ep___charts_users.default };
-const $clips_addNote: Provider = { provide: 'ep:clips/add-note', useClass: ep___clips_addNote.default };
-const $clips_removeNote: Provider = { provide: 'ep:clips/remove-note', useClass: ep___clips_removeNote.default };
-const $clips_create: Provider = { provide: 'ep:clips/create', useClass: ep___clips_create.default };
-const $clips_delete: Provider = { provide: 'ep:clips/delete', useClass: ep___clips_delete.default };
-const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_list.default };
-const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default };
-const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default };
-const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default };
-const $clips_favorite: Provider = { provide: 'ep:clips/favorite', useClass: ep___clips_favorite.default };
-const $clips_unfavorite: Provider = { provide: 'ep:clips/unfavorite', useClass: ep___clips_unfavorite.default };
-const $clips_myFavorites: Provider = { provide: 'ep:clips/my-favorites', useClass: ep___clips_myFavorites.default };
-const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default };
-const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default };
-const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default };
-const $drive_files_checkExistence: Provider = { provide: 'ep:drive/files/check-existence', useClass: ep___drive_files_checkExistence.default };
-const $drive_files_create: Provider = { provide: 'ep:drive/files/create', useClass: ep___drive_files_create.default };
-const $drive_files_delete: Provider = { provide: 'ep:drive/files/delete', useClass: ep___drive_files_delete.default };
-const $drive_files_findByHash: Provider = { provide: 'ep:drive/files/find-by-hash', useClass: ep___drive_files_findByHash.default };
-const $drive_files_find: Provider = { provide: 'ep:drive/files/find', useClass: ep___drive_files_find.default };
-const $drive_files_show: Provider = { provide: 'ep:drive/files/show', useClass: ep___drive_files_show.default };
-const $drive_files_update: Provider = { provide: 'ep:drive/files/update', useClass: ep___drive_files_update.default };
-const $drive_files_uploadFromUrl: Provider = { provide: 'ep:drive/files/upload-from-url', useClass: ep___drive_files_uploadFromUrl.default };
-const $drive_folders: Provider = { provide: 'ep:drive/folders', useClass: ep___drive_folders.default };
-const $drive_folders_create: Provider = { provide: 'ep:drive/folders/create', useClass: ep___drive_folders_create.default };
-const $drive_folders_delete: Provider = { provide: 'ep:drive/folders/delete', useClass: ep___drive_folders_delete.default };
-const $drive_folders_find: Provider = { provide: 'ep:drive/folders/find', useClass: ep___drive_folders_find.default };
-const $drive_folders_show: Provider = { provide: 'ep:drive/folders/show', useClass: ep___drive_folders_show.default };
-const $drive_folders_update: Provider = { provide: 'ep:drive/folders/update', useClass: ep___drive_folders_update.default };
-const $drive_stream: Provider = { provide: 'ep:drive/stream', useClass: ep___drive_stream.default };
-const $emailAddress_available: Provider = { provide: 'ep:email-address/available', useClass: ep___emailAddress_available.default };
-const $endpoint: Provider = { provide: 'ep:endpoint', useClass: ep___endpoint.default };
-const $endpoints: Provider = { provide: 'ep:endpoints', useClass: ep___endpoints.default };
-const $exportCustomEmojis: Provider = { provide: 'ep:export-custom-emojis', useClass: ep___exportCustomEmojis.default };
-const $federation_followers: Provider = { provide: 'ep:federation/followers', useClass: ep___federation_followers.default };
-const $federation_following: Provider = { provide: 'ep:federation/following', useClass: ep___federation_following.default };
-const $federation_instances: Provider = { provide: 'ep:federation/instances', useClass: ep___federation_instances.default };
-const $federation_showInstance: Provider = { provide: 'ep:federation/show-instance', useClass: ep___federation_showInstance.default };
-const $federation_updateRemoteUser: Provider = { provide: 'ep:federation/update-remote-user', useClass: ep___federation_updateRemoteUser.default };
-const $federation_users: Provider = { provide: 'ep:federation/users', useClass: ep___federation_users.default };
-const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default };
-const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
-const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
-const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default };
-const $following_update_all: Provider = { provide: 'ep:following/update-all', useClass: ep___following_update_all.default };
-const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
-const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
-const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
-const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default };
-const $following_requests_sent: Provider = { provide: 'ep:following/requests/sent', useClass: ep___following_requests_sent.default };
-const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default };
-const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default };
-const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default };
-const $gallery_posts: Provider = { provide: 'ep:gallery/posts', useClass: ep___gallery_posts.default };
-const $gallery_posts_create: Provider = { provide: 'ep:gallery/posts/create', useClass: ep___gallery_posts_create.default };
-const $gallery_posts_delete: Provider = { provide: 'ep:gallery/posts/delete', useClass: ep___gallery_posts_delete.default };
-const $gallery_posts_like: Provider = { provide: 'ep:gallery/posts/like', useClass: ep___gallery_posts_like.default };
-const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useClass: ep___gallery_posts_show.default };
-const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default };
-const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default };
-const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default };
-const $getAvatarDecorations: Provider = { provide: 'ep:get-avatar-decorations', useClass: ep___getAvatarDecorations.default };
-const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default };
-const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default };
-const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default };
-const $hashtags_trend: Provider = { provide: 'ep:hashtags/trend', useClass: ep___hashtags_trend.default };
-const $hashtags_users: Provider = { provide: 'ep:hashtags/users', useClass: ep___hashtags_users.default };
-const $i: Provider = { provide: 'ep:i', useClass: ep___i.default };
-const $i_2fa_done: Provider = { provide: 'ep:i/2fa/done', useClass: ep___i_2fa_done.default };
-const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___i_2fa_keyDone.default };
-const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default };
-const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default };
-const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default };
-const $i_2fa_updateKey: Provider = { provide: 'ep:i/2fa/update-key', useClass: ep___i_2fa_updateKey.default };
-const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default };
-const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
-const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
-const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default };
-const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default };
-const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default };
-const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default };
-const $i_exportData: Provider = { provide: 'ep:i/export-data', useClass: ep___i_exportData.default };
-const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default };
-const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
-const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
-const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default };
-const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default };
-const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default };
-const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default };
-const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default };
-const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default };
-const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default };
-const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default };
-const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default };
-const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default };
-const $i_importNotes: Provider = { provide: 'ep:i/import-notes', useClass: ep___i_importNotes.default };
-const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default };
-const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default };
-const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default };
-const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default };
-const $i_notificationsGrouped: Provider = { provide: 'ep:i/notifications-grouped', useClass: ep___i_notificationsGrouped.default };
-const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default };
-const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default };
-const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default };
-const $i_readAllUnreadNotes: Provider = { provide: 'ep:i/read-all-unread-notes', useClass: ep___i_readAllUnreadNotes.default };
-const $i_readAnnouncement: Provider = { provide: 'ep:i/read-announcement', useClass: ep___i_readAnnouncement.default };
-const $i_regenerateToken: Provider = { provide: 'ep:i/regenerate-token', useClass: ep___i_regenerateToken.default };
-const $i_registry_getAll: Provider = { provide: 'ep:i/registry/get-all', useClass: ep___i_registry_getAll.default };
-const $i_registry_getUnsecure: Provider = { provide: 'ep:i/registry/get-unsecure', useClass: ep___i_registry_getUnsecure.default };
-const $i_registry_getDetail: Provider = { provide: 'ep:i/registry/get-detail', useClass: ep___i_registry_getDetail.default };
-const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep___i_registry_get.default };
-const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default };
-const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default };
-const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default };
-const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default };
-const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default };
-const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default };
-const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default };
-const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default };
-const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default };
-const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default };
-const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default };
-const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default };
-const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default };
-const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
-const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
-const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
-const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
-const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
-const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
-const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
-const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default };
-const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
-const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
-const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
-const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
-const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
-const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default };
-const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default };
-const $renoteMute_create: Provider = { provide: 'ep:renote-mute/create', useClass: ep___renoteMute_create.default };
-const $renoteMute_delete: Provider = { provide: 'ep:renote-mute/delete', useClass: ep___renoteMute_delete.default };
-const $renoteMute_list: Provider = { provide: 'ep:renote-mute/list', useClass: ep___renoteMute_list.default };
-const $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default };
-const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default };
-const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.default };
-const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default };
-const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
-const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
-const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
-const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
-const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
-const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
-const $notes_following: Provider = { provide: 'ep:notes/following', useClass: ep___notes_following.default };
-const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
-const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default };
-const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default };
-const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default };
-const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
-const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default };
-const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default };
-const $notes_polls_refresh: Provider = { provide: 'ep:notes/polls/refresh', useClass: ep___notes_polls_refresh.default };
-const $notes_reactions: Provider = { provide: 'ep:notes/reactions', useClass: ep___notes_reactions.default };
-const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create', useClass: ep___notes_reactions_create.default };
-const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default };
-const $notes_like: Provider = { provide: 'ep:notes/like', useClass: ep___notes_like.default };
-const $notes_renotes: Provider = { provide: 'ep:notes/renotes', useClass: ep___notes_renotes.default };
-const $notes_replies: Provider = { provide: 'ep:notes/replies', useClass: ep___notes_replies.default };
-const $notes_schedule_create: Provider = { provide: 'ep:notes/schedule/create', useClass: ep___notes_schedule_create.default };
-const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default };
-const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default };
-const $notes_searchByTag: Provider = { provide: 'ep:notes/search-by-tag', useClass: ep___notes_searchByTag.default };
-const $notes_search: Provider = { provide: 'ep:notes/search', useClass: ep___notes_search.default };
-const $notes_show: Provider = { provide: 'ep:notes/show', useClass: ep___notes_show.default };
-const $notes_state: Provider = { provide: 'ep:notes/state', useClass: ep___notes_state.default };
-const $notes_threadMuting_create: Provider = { provide: 'ep:notes/thread-muting/create', useClass: ep___notes_threadMuting_create.default };
-const $notes_threadMuting_delete: Provider = { provide: 'ep:notes/thread-muting/delete', useClass: ep___notes_threadMuting_delete.default };
-const $notes_timeline: Provider = { provide: 'ep:notes/timeline', useClass: ep___notes_timeline.default };
-const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep___notes_translate.default };
-const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default };
-const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
-const $notes_edit: Provider = { provide: 'ep:notes/edit', useClass: ep___notes_edit.default };
-const $notes_versions: Provider = { provide: 'ep:notes/versions', useClass: ep___notes_versions.default };
-const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
-const $notifications_flush: Provider = { provide: 'ep:notifications/flush', useClass: ep___notifications_flush.default };
-const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
-const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default };
-const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
-const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
-const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
-const $pages_featured: Provider = { provide: 'ep:pages/featured', useClass: ep___pages_featured.default };
-const $pages_like: Provider = { provide: 'ep:pages/like', useClass: ep___pages_like.default };
-const $pages_show: Provider = { provide: 'ep:pages/show', useClass: ep___pages_show.default };
-const $pages_unlike: Provider = { provide: 'ep:pages/unlike', useClass: ep___pages_unlike.default };
-const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pages_update.default };
-const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default };
-const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default };
-const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default };
-const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default };
-const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default };
-const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default };
-const $flash_update: Provider = { provide: 'ep:flash/update', useClass: ep___flash_update.default };
-const $flash_my: Provider = { provide: 'ep:flash/my', useClass: ep___flash_my.default };
-const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___flash_myLikes.default };
-const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default };
-const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default };
-const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default };
-const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default };
-const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default };
-const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default };
-const $roles_notes: Provider = { provide: 'ep:roles/notes', useClass: ep___roles_notes.default };
-const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default };
-const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default };
-const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
-const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default };
-const $stats: Provider = { provide: 'ep:stats', useClass: ep___stats.default };
-const $sw_show_registration: Provider = { provide: 'ep:sw/show-registration', useClass: ep___sw_show_registration.default };
-const $sw_update_registration: Provider = { provide: 'ep:sw/update-registration', useClass: ep___sw_update_registration.default };
-const $sw_register: Provider = { provide: 'ep:sw/register', useClass: ep___sw_register.default };
-const $sw_unregister: Provider = { provide: 'ep:sw/unregister', useClass: ep___sw_unregister.default };
-const $test: Provider = { provide: 'ep:test', useClass: ep___test.default };
-const $username_available: Provider = { provide: 'ep:username/available', useClass: ep___username_available.default };
-const $users: Provider = { provide: 'ep:users', useClass: ep___users.default };
-const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users_clips.default };
-const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default };
-const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default };
-const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default };
-const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
-const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default };
-const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
-const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default };
-const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default };
-const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: ep___users_lists_pull.default };
-const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default };
-const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default };
-const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
-const $users_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_createFromPublic: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_createFromPublic.default };
-const $users_lists_updateMembership: Provider = { provide: 'ep:users/lists/update-membership', useClass: ep___users_lists_updateMembership.default };
-const $users_lists_getMemberships: Provider = { provide: 'ep:users/lists/get-memberships', useClass: ep___users_lists_getMemberships.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_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default };
-const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default };
-const $users_recommendation: Provider = { provide: 'ep:users/recommendation', useClass: ep___users_recommendation.default };
-const $users_relation: Provider = { provide: 'ep:users/relation', useClass: ep___users_relation.default };
-const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClass: ep___users_reportAbuse.default };
-const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default };
-const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
-const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
-const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
-const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
-const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
-const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
-const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
-const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.default };
-const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default };
-const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default };
-const $reversi_cancelMatch: Provider = { provide: 'ep:reversi/cancel-match', useClass: ep___reversi_cancelMatch.default };
-const $reversi_games: Provider = { provide: 'ep:reversi/games', useClass: ep___reversi_games.default };
-const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___reversi_match.default };
-const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default };
-const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default };
-const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default };
-const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default };
+const endpoints = Object.entries(endpointsObject);
+const endpointProviders = endpoints.map(([path, endpoint]): Provider => ({ provide: `ep:${path}`, useClass: endpoint.default }));
@Module({
imports: [
@@ -824,811 +21,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
providers: [
GetterService,
ApiLoggerService,
- $admin_meta,
- $admin_abuseUserReports,
- $admin_abuseReport_notificationRecipient_list,
- $admin_abuseReport_notificationRecipient_show,
- $admin_abuseReport_notificationRecipient_create,
- $admin_abuseReport_notificationRecipient_update,
- $admin_abuseReport_notificationRecipient_delete,
- $admin_accounts_create,
- $admin_accounts_delete,
- $admin_accounts_findByEmail,
- $admin_ad_create,
- $admin_ad_delete,
- $admin_ad_list,
- $admin_ad_update,
- $admin_announcements_create,
- $admin_announcements_delete,
- $admin_announcements_list,
- $admin_announcements_update,
- $admin_avatarDecorations_create,
- $admin_avatarDecorations_delete,
- $admin_avatarDecorations_list,
- $admin_avatarDecorations_update,
- $admin_deleteAllFilesOfAUser,
- $admin_unsetUserAvatar,
- $admin_unsetUserBanner,
- $admin_drive_cleanRemoteFiles,
- $admin_drive_cleanup,
- $admin_drive_files,
- $admin_drive_showFile,
- $admin_emoji_addAliasesBulk,
- $admin_emoji_add,
- $admin_emoji_copy,
- $admin_emoji_deleteBulk,
- $admin_emoji_delete,
- $admin_emoji_importZip,
- $admin_emoji_listRemote,
- $admin_emoji_list,
- $admin_emoji_removeAliasesBulk,
- $admin_emoji_setAliasesBulk,
- $admin_emoji_setCategoryBulk,
- $admin_emoji_setLicenseBulk,
- $admin_emoji_update,
- $admin_federation_deleteAllFiles,
- $admin_federation_refreshRemoteInstanceMetadata,
- $admin_federation_removeAllFollowing,
- $admin_federation_updateInstance,
- $admin_getIndexStats,
- $admin_getTableStats,
- $admin_getUserIps,
- $admin_invite_create,
- $admin_invite_list,
- $admin_promo_create,
- $admin_queue_clear,
- $admin_queue_deliverDelayed,
- $admin_queue_inboxDelayed,
- $admin_queue_promote,
- $admin_queue_stats,
- $admin_relays_add,
- $admin_relays_list,
- $admin_relays_remove,
- $admin_resetPassword,
- $admin_resolveAbuseUserReport,
- $admin_forwardAbuseUserReport,
- $admin_updateAbuseUserReport,
- $admin_sendEmail,
- $admin_serverInfo,
- $admin_showModerationLogs,
- $admin_showUser,
- $admin_showUsers,
- $admin_nsfwUser,
- $admin_unnsfwUser,
- $admin_silenceUser,
- $admin_unsilenceUser,
- $admin_suspendUser,
- $admin_approveUser,
- $admin_declineUser,
- $admin_unsuspendUser,
- $admin_updateMeta,
- $admin_deleteAccount,
- $admin_updateUserNote,
- $admin_roles_create,
- $admin_roles_delete,
- $admin_roles_list,
- $admin_roles_show,
- $admin_roles_update,
- $admin_roles_assign,
- $admin_roles_unassign,
- $admin_roles_updateDefaultPolicies,
- $admin_roles_users,
- $admin_systemWebhook_create,
- $admin_systemWebhook_delete,
- $admin_systemWebhook_list,
- $admin_systemWebhook_show,
- $admin_systemWebhook_update,
- $admin_systemWebhook_test,
- $announcements,
- $announcements_show,
- $antennas_create,
- $antennas_delete,
- $antennas_list,
- $antennas_notes,
- $antennas_show,
- $antennas_update,
- $ap_get,
- $ap_show,
- $app_create,
- $app_show,
- $auth_accept,
- $auth_session_generate,
- $auth_session_show,
- $auth_session_userkey,
- $blocking_create,
- $blocking_delete,
- $blocking_list,
- $channels_create,
- $channels_featured,
- $channels_follow,
- $channels_followed,
- $channels_owned,
- $channels_show,
- $channels_timeline,
- $channels_unfollow,
- $channels_update,
- $channels_favorite,
- $channels_unfavorite,
- $channels_myFavorites,
- $channels_search,
- $charts_activeUsers,
- $charts_apRequest,
- $charts_drive,
- $charts_federation,
- $charts_instance,
- $charts_notes,
- $charts_user_drive,
- $charts_user_following,
- $charts_user_notes,
- $charts_user_pv,
- $charts_user_reactions,
- $charts_users,
- $clips_addNote,
- $clips_removeNote,
- $clips_create,
- $clips_delete,
- $clips_list,
- $clips_notes,
- $clips_show,
- $clips_update,
- $clips_favorite,
- $clips_unfavorite,
- $clips_myFavorites,
- $drive,
- $drive_files,
- $drive_files_attachedNotes,
- $drive_files_checkExistence,
- $drive_files_create,
- $drive_files_delete,
- $drive_files_findByHash,
- $drive_files_find,
- $drive_files_show,
- $drive_files_update,
- $drive_files_uploadFromUrl,
- $drive_folders,
- $drive_folders_create,
- $drive_folders_delete,
- $drive_folders_find,
- $drive_folders_show,
- $drive_folders_update,
- $drive_stream,
- $emailAddress_available,
- $endpoint,
- $endpoints,
- $exportCustomEmojis,
- $federation_followers,
- $federation_following,
- $federation_instances,
- $federation_showInstance,
- $federation_updateRemoteUser,
- $federation_users,
- $federation_stats,
- $following_create,
- $following_delete,
- $following_update,
- $following_update_all,
- $following_invalidate,
- $following_requests_accept,
- $following_requests_cancel,
- $following_requests_list,
- $following_requests_sent,
- $following_requests_reject,
- $gallery_featured,
- $gallery_popular,
- $gallery_posts,
- $gallery_posts_create,
- $gallery_posts_delete,
- $gallery_posts_like,
- $gallery_posts_show,
- $gallery_posts_unlike,
- $gallery_posts_update,
- $getOnlineUsersCount,
- $getAvatarDecorations,
- $hashtags_list,
- $hashtags_search,
- $hashtags_show,
- $hashtags_trend,
- $hashtags_users,
- $i,
- $i_2fa_done,
- $i_2fa_keyDone,
- $i_2fa_passwordLess,
- $i_2fa_registerKey,
- $i_2fa_register,
- $i_2fa_updateKey,
- $i_2fa_removeKey,
- $i_2fa_unregister,
- $i_apps,
- $i_authorizedApps,
- $i_claimAchievement,
- $i_changePassword,
- $i_deleteAccount,
- $i_exportData,
- $i_exportBlocking,
- $i_exportFollowing,
- $i_exportMute,
- $i_exportNotes,
- $i_exportClips,
- $i_exportFavorites,
- $i_exportUserLists,
- $i_exportAntennas,
- $i_favorites,
- $i_gallery_likes,
- $i_gallery_posts,
- $i_importBlocking,
- $i_importFollowing,
- $i_importNotes,
- $i_importMuting,
- $i_importUserLists,
- $i_importAntennas,
- $i_notifications,
- $i_notificationsGrouped,
- $i_pageLikes,
- $i_pages,
- $i_pin,
- $i_readAllUnreadNotes,
- $i_readAnnouncement,
- $i_regenerateToken,
- $i_registry_getAll,
- $i_registry_getUnsecure,
- $i_registry_getDetail,
- $i_registry_get,
- $i_registry_keysWithType,
- $i_registry_keys,
- $i_registry_remove,
- $i_registry_scopesWithDomain,
- $i_registry_set,
- $i_revokeToken,
- $i_signinHistory,
- $i_unpin,
- $i_updateEmail,
- $i_update,
- $i_move,
- $i_webhooks_create,
- $i_webhooks_list,
- $i_webhooks_show,
- $i_webhooks_update,
- $i_webhooks_delete,
- $i_webhooks_test,
- $invite_create,
- $invite_delete,
- $invite_list,
- $invite_limit,
- $meta,
- $emojis,
- $emoji,
- $miauth_genToken,
- $mute_create,
- $mute_delete,
- $mute_list,
- $renoteMute_create,
- $renoteMute_delete,
- $renoteMute_list,
- $my_apps,
- $notes,
- $notes_children,
- $notes_clips,
- $notes_conversation,
- $notes_create,
- $notes_delete,
- $notes_favorites_create,
- $notes_favorites_delete,
- $notes_featured,
- $notes_following,
- $notes_globalTimeline,
- $notes_bubbleTimeline,
- $notes_hybridTimeline,
- $notes_localTimeline,
- $notes_mentions,
- $notes_polls_recommendation,
- $notes_polls_vote,
- $notes_polls_refresh,
- $notes_reactions,
- $notes_reactions_create,
- $notes_reactions_delete,
- $notes_like,
- $notes_renotes,
- $notes_replies,
- $notes_schedule_create,
- $notes_schedule_delete,
- $notes_schedule_list,
- $notes_searchByTag,
- $notes_search,
- $notes_show,
- $notes_state,
- $notes_threadMuting_create,
- $notes_threadMuting_delete,
- $notes_timeline,
- $notes_translate,
- $notes_unrenote,
- $notes_userListTimeline,
- $notes_edit,
- $notes_versions,
- $notifications_create,
- $notifications_flush,
- $notifications_markAllAsRead,
- $notifications_testNotification,
- $pagePush,
- $pages_create,
- $pages_delete,
- $pages_featured,
- $pages_like,
- $pages_show,
- $pages_unlike,
- $pages_update,
- $flash_create,
- $flash_delete,
- $flash_featured,
- $flash_like,
- $flash_show,
- $flash_unlike,
- $flash_update,
- $flash_my,
- $flash_myLikes,
- $ping,
- $pinnedUsers,
- $promo_read,
- $roles_list,
- $roles_show,
- $roles_users,
- $roles_notes,
- $requestResetPassword,
- $resetDb,
- $resetPassword,
- $serverInfo,
- $stats,
- $sw_show_registration,
- $sw_update_registration,
- $sw_register,
- $sw_unregister,
- $test,
- $username_available,
- $users,
- $users_clips,
- $users_followers,
- $users_following,
- $users_gallery_posts,
- $users_getFrequentlyRepliedUsers,
- $users_featuredNotes,
- $users_lists_create,
- $users_lists_delete,
- $users_lists_list,
- $users_lists_pull,
- $users_lists_push,
- $users_lists_show,
- $users_lists_update,
- $users_lists_favorite,
- $users_lists_unfavorite,
- $users_lists_createFromPublic,
- $users_lists_updateMembership,
- $users_lists_getMemberships,
- $users_notes,
- $users_pages,
- $users_flashs,
- $users_reactions,
- $users_recommendation,
- $users_relation,
- $users_reportAbuse,
- $users_searchByUsernameAndHost,
- $users_search,
- $users_show,
- $users_achievements,
- $users_updateMemo,
- $fetchRss,
- $fetchExternalResources,
- $retention,
- $sponsors,
- $bubbleGame_register,
- $bubbleGame_ranking,
- $reversi_cancelMatch,
- $reversi_games,
- $reversi_match,
- $reversi_invitations,
- $reversi_showGame,
- $reversi_surrender,
- $reversi_verify,
+ ...endpointProviders,
],
exports: [
- $admin_meta,
- $admin_abuseUserReports,
- $admin_abuseReport_notificationRecipient_list,
- $admin_abuseReport_notificationRecipient_show,
- $admin_abuseReport_notificationRecipient_create,
- $admin_abuseReport_notificationRecipient_update,
- $admin_abuseReport_notificationRecipient_delete,
- $admin_accounts_create,
- $admin_accounts_delete,
- $admin_accounts_findByEmail,
- $admin_ad_create,
- $admin_ad_delete,
- $admin_ad_list,
- $admin_ad_update,
- $admin_announcements_create,
- $admin_announcements_delete,
- $admin_announcements_list,
- $admin_announcements_update,
- $admin_avatarDecorations_create,
- $admin_avatarDecorations_delete,
- $admin_avatarDecorations_list,
- $admin_avatarDecorations_update,
- $admin_deleteAllFilesOfAUser,
- $admin_unsetUserAvatar,
- $admin_unsetUserBanner,
- $admin_drive_cleanRemoteFiles,
- $admin_drive_cleanup,
- $admin_drive_files,
- $admin_drive_showFile,
- $admin_emoji_addAliasesBulk,
- $admin_emoji_add,
- $admin_emoji_copy,
- $admin_emoji_deleteBulk,
- $admin_emoji_delete,
- $admin_emoji_importZip,
- $admin_emoji_listRemote,
- $admin_emoji_list,
- $admin_emoji_removeAliasesBulk,
- $admin_emoji_setAliasesBulk,
- $admin_emoji_setCategoryBulk,
- $admin_emoji_setLicenseBulk,
- $admin_emoji_update,
- $admin_federation_deleteAllFiles,
- $admin_federation_refreshRemoteInstanceMetadata,
- $admin_federation_removeAllFollowing,
- $admin_federation_updateInstance,
- $admin_getIndexStats,
- $admin_getTableStats,
- $admin_getUserIps,
- $admin_invite_create,
- $admin_invite_list,
- $admin_promo_create,
- $admin_queue_clear,
- $admin_queue_deliverDelayed,
- $admin_queue_inboxDelayed,
- $admin_queue_promote,
- $admin_queue_stats,
- $admin_relays_add,
- $admin_relays_list,
- $admin_relays_remove,
- $admin_resetPassword,
- $admin_resolveAbuseUserReport,
- $admin_forwardAbuseUserReport,
- $admin_updateAbuseUserReport,
- $admin_sendEmail,
- $admin_serverInfo,
- $admin_showModerationLogs,
- $admin_showUser,
- $admin_showUsers,
- $admin_nsfwUser,
- $admin_unnsfwUser,
- $admin_silenceUser,
- $admin_unsilenceUser,
- $admin_suspendUser,
- $admin_approveUser,
- $admin_declineUser,
- $admin_unsuspendUser,
- $admin_updateMeta,
- $admin_deleteAccount,
- $admin_updateUserNote,
- $admin_roles_create,
- $admin_roles_delete,
- $admin_roles_list,
- $admin_roles_show,
- $admin_roles_update,
- $admin_roles_assign,
- $admin_roles_unassign,
- $admin_roles_updateDefaultPolicies,
- $admin_roles_users,
- $admin_systemWebhook_create,
- $admin_systemWebhook_delete,
- $admin_systemWebhook_list,
- $admin_systemWebhook_show,
- $admin_systemWebhook_update,
- $admin_systemWebhook_test,
- $announcements,
- $announcements_show,
- $antennas_create,
- $antennas_delete,
- $antennas_list,
- $antennas_notes,
- $antennas_show,
- $antennas_update,
- $ap_get,
- $ap_show,
- $app_create,
- $app_show,
- $auth_accept,
- $auth_session_generate,
- $auth_session_show,
- $auth_session_userkey,
- $blocking_create,
- $blocking_delete,
- $blocking_list,
- $channels_create,
- $channels_featured,
- $channels_follow,
- $channels_followed,
- $channels_owned,
- $channels_show,
- $channels_timeline,
- $channels_unfollow,
- $channels_update,
- $channels_favorite,
- $channels_unfavorite,
- $channels_myFavorites,
- $channels_search,
- $charts_activeUsers,
- $charts_apRequest,
- $charts_drive,
- $charts_federation,
- $charts_instance,
- $charts_notes,
- $charts_user_drive,
- $charts_user_following,
- $charts_user_notes,
- $charts_user_pv,
- $charts_user_reactions,
- $charts_users,
- $clips_addNote,
- $clips_removeNote,
- $clips_create,
- $clips_delete,
- $clips_list,
- $clips_notes,
- $clips_show,
- $clips_update,
- $clips_favorite,
- $clips_unfavorite,
- $clips_myFavorites,
- $drive,
- $drive_files,
- $drive_files_attachedNotes,
- $drive_files_checkExistence,
- $drive_files_create,
- $drive_files_delete,
- $drive_files_findByHash,
- $drive_files_find,
- $drive_files_show,
- $drive_files_update,
- $drive_files_uploadFromUrl,
- $drive_folders,
- $drive_folders_create,
- $drive_folders_delete,
- $drive_folders_find,
- $drive_folders_show,
- $drive_folders_update,
- $drive_stream,
- $emailAddress_available,
- $endpoint,
- $endpoints,
- $exportCustomEmojis,
- $federation_followers,
- $federation_following,
- $federation_instances,
- $federation_showInstance,
- $federation_updateRemoteUser,
- $federation_users,
- $federation_stats,
- $following_create,
- $following_delete,
- $following_update,
- $following_update_all,
- $following_invalidate,
- $following_requests_accept,
- $following_requests_cancel,
- $following_requests_list,
- $following_requests_reject,
- $gallery_featured,
- $gallery_popular,
- $gallery_posts,
- $gallery_posts_create,
- $gallery_posts_delete,
- $gallery_posts_like,
- $gallery_posts_show,
- $gallery_posts_unlike,
- $gallery_posts_update,
- $getOnlineUsersCount,
- $getAvatarDecorations,
- $hashtags_list,
- $hashtags_search,
- $hashtags_show,
- $hashtags_trend,
- $hashtags_users,
- $i,
- $i_2fa_done,
- $i_2fa_keyDone,
- $i_2fa_passwordLess,
- $i_2fa_registerKey,
- $i_2fa_register,
- $i_2fa_updateKey,
- $i_2fa_removeKey,
- $i_2fa_unregister,
- $i_apps,
- $i_authorizedApps,
- $i_claimAchievement,
- $i_changePassword,
- $i_deleteAccount,
- $i_exportData,
- $i_exportBlocking,
- $i_exportFollowing,
- $i_exportMute,
- $i_exportNotes,
- $i_exportClips,
- $i_exportFavorites,
- $i_exportUserLists,
- $i_exportAntennas,
- $i_favorites,
- $i_gallery_likes,
- $i_gallery_posts,
- $i_importBlocking,
- $i_importFollowing,
- $i_importNotes,
- $i_importMuting,
- $i_importUserLists,
- $i_importAntennas,
- $i_notifications,
- $i_notificationsGrouped,
- $i_pageLikes,
- $i_pages,
- $i_pin,
- $i_readAllUnreadNotes,
- $i_readAnnouncement,
- $i_regenerateToken,
- $i_registry_getAll,
- $i_registry_getUnsecure,
- $i_registry_getDetail,
- $i_registry_get,
- $i_registry_keysWithType,
- $i_registry_keys,
- $i_registry_remove,
- $i_registry_scopesWithDomain,
- $i_registry_set,
- $i_revokeToken,
- $i_signinHistory,
- $i_unpin,
- $i_updateEmail,
- $i_update,
- $i_move,
- $i_webhooks_create,
- $i_webhooks_list,
- $i_webhooks_show,
- $i_webhooks_update,
- $i_webhooks_delete,
- $i_webhooks_test,
- $invite_create,
- $invite_delete,
- $invite_list,
- $invite_limit,
- $meta,
- $emojis,
- $emoji,
- $miauth_genToken,
- $mute_create,
- $mute_delete,
- $mute_list,
- $renoteMute_create,
- $renoteMute_delete,
- $renoteMute_list,
- $my_apps,
- $notes,
- $notes_children,
- $notes_clips,
- $notes_conversation,
- $notes_create,
- $notes_delete,
- $notes_favorites_create,
- $notes_favorites_delete,
- $notes_featured,
- $notes_following,
- $notes_globalTimeline,
- $notes_bubbleTimeline,
- $notes_hybridTimeline,
- $notes_localTimeline,
- $notes_mentions,
- $notes_polls_recommendation,
- $notes_polls_vote,
- $notes_polls_refresh,
- $notes_reactions,
- $notes_reactions_create,
- $notes_reactions_delete,
- $notes_like,
- $notes_renotes,
- $notes_replies,
- $notes_schedule_create,
- $notes_schedule_delete,
- $notes_schedule_list,
- $notes_searchByTag,
- $notes_search,
- $notes_show,
- $notes_state,
- $notes_threadMuting_create,
- $notes_threadMuting_delete,
- $notes_timeline,
- $notes_translate,
- $notes_unrenote,
- $notes_userListTimeline,
- $notes_edit,
- $notes_versions,
- $notifications_create,
- $notifications_flush,
- $notifications_markAllAsRead,
- $notifications_testNotification,
- $pagePush,
- $pages_create,
- $pages_delete,
- $pages_featured,
- $pages_like,
- $pages_show,
- $pages_unlike,
- $pages_update,
- $flash_create,
- $flash_delete,
- $flash_featured,
- $flash_like,
- $flash_show,
- $flash_unlike,
- $flash_update,
- $flash_my,
- $flash_myLikes,
- $ping,
- $pinnedUsers,
- $promo_read,
- $roles_list,
- $roles_show,
- $roles_users,
- $roles_notes,
- $requestResetPassword,
- $resetDb,
- $resetPassword,
- $serverInfo,
- $stats,
- $sw_register,
- $sw_unregister,
- $test,
- $username_available,
- $users,
- $users_clips,
- $users_followers,
- $users_following,
- $users_gallery_posts,
- $users_getFrequentlyRepliedUsers,
- $users_featuredNotes,
- $users_lists_create,
- $users_lists_delete,
- $users_lists_list,
- $users_lists_pull,
- $users_lists_push,
- $users_lists_show,
- $users_lists_update,
- $users_lists_favorite,
- $users_lists_unfavorite,
- $users_lists_createFromPublic,
- $users_lists_updateMembership,
- $users_lists_getMemberships,
- $users_notes,
- $users_pages,
- $users_flashs,
- $users_reactions,
- $users_recommendation,
- $users_relation,
- $users_reportAbuse,
- $users_searchByUsernameAndHost,
- $users_search,
- $users_show,
- $users_achievements,
- $users_updateMemo,
- $fetchRss,
- $fetchExternalResources,
- $retention,
- $sponsors,
- $bubbleGame_register,
- $bubbleGame_ranking,
- $reversi_cancelMatch,
- $reversi_games,
- $reversi_match,
- $reversi_invitations,
- $reversi_showGame,
- $reversi_surrender,
- $reversi_verify,
+ ...endpointProviders,
],
})
export class EndpointsModule {}
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index fa9155d82d..72712bce60 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -26,12 +26,19 @@ import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
-import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
+import { Keyed, RateLimit, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
+// Up to 10 attempts, then 1 per minute
+const signinRateLimit: Keyed<RateLimit> = {
+ key: 'signin',
+ max: 10,
+ dripRate: 1000 * 60,
+};
+
@Injectable()
export class SigninApiService {
constructor(
@@ -94,7 +101,7 @@ export class SigninApiService {
}
// not more than 1 attempt per second and not more than 10 attempts per hour
- const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
+ const rateLimit = await this.rateLimiterService.limit(signinRateLimit, getIpHash(request.ip));
sendRateLimitHeaders(reply, rateLimit);
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
index e94d2b6b68..f84f50523b 100644
--- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -21,7 +21,7 @@ import { WebAuthnService } from '@/core/WebAuthnService.js';
import Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IdentifiableError } from '@/misc/identifiable-error.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 7aea6a0e56..42137d3298 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -19,10 +19,9 @@ import { MiLocalUser } from '@/models/User.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
+import { RoleService } from '@/core/RoleService.js';
import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
-import instance from './endpoints/charts/instance.js';
-import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class SignupApiService {
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index e3fd1312ae..6e7abcfae6 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -18,10 +18,9 @@ import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js';
import { UserService } from '@/core/UserService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
-import { RoleService } from '@/core/RoleService.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import { LoggerService } from '@/core/LoggerService.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/Connection.js';
import { ChannelsService } from './stream/ChannelsService.js';
@@ -49,7 +48,6 @@ export class StreamingApiServerService {
private usersService: UserService,
private channelFollowingService: ChannelFollowingService,
private rateLimiterService: SkRateLimiterService,
- private roleService: RoleService,
private loggerService: LoggerService,
) {
}
@@ -57,22 +55,18 @@ export class StreamingApiServerService {
@bindThis
private async rateLimitThis(
user: MiLocalUser | null | undefined,
- requestIp: string | undefined,
+ requestIp: string,
limit: IEndpointMeta['limit'] & { key: NonNullable<string> },
) : Promise<boolean> {
- let limitActor: string;
+ let limitActor: string | MiLocalUser;
if (user) {
- limitActor = user.id;
+ limitActor = user;
} else {
- limitActor = getIpHash(requestIp || 'wtf');
+ limitActor = getIpHash(requestIp);
}
- const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
-
- if (factor <= 0) return false;
-
// Rate limit
- const rateLimit = await this.rateLimiterService.limit(limit, limitActor, factor);
+ const rateLimit = await this.rateLimiterService.limit(limit, limitActor);
return rateLimit.blocked;
}
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
new file mode 100644
index 0000000000..a641a14448
--- /dev/null
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -0,0 +1,421 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/*
+ * This file contains list of all endpoints exported as pathname of API endpoint
+ *
+ * When you add new endpoint, you should add it to this file.
+ * This file is used to generate API documentation and EndpointsModule.
+ */
+
+export * as 'admin/abuse-report/notification-recipient/create' from './endpoints/admin/abuse-report/notification-recipient/create.js';
+export * as 'admin/abuse-report/notification-recipient/delete' from './endpoints/admin/abuse-report/notification-recipient/delete.js';
+export * as 'admin/abuse-report/notification-recipient/list' from './endpoints/admin/abuse-report/notification-recipient/list.js';
+export * as 'admin/abuse-report/notification-recipient/show' from './endpoints/admin/abuse-report/notification-recipient/show.js';
+export * as 'admin/abuse-report/notification-recipient/update' from './endpoints/admin/abuse-report/notification-recipient/update.js';
+export * as 'admin/abuse-user-reports' from './endpoints/admin/abuse-user-reports.js';
+export * as 'admin/accounts/create' from './endpoints/admin/accounts/create.js';
+export * as 'admin/accounts/delete' from './endpoints/admin/accounts/delete.js';
+export * as 'admin/accounts/find-by-email' from './endpoints/admin/accounts/find-by-email.js';
+export * as 'admin/ad/create' from './endpoints/admin/ad/create.js';
+export * as 'admin/ad/delete' from './endpoints/admin/ad/delete.js';
+export * as 'admin/ad/list' from './endpoints/admin/ad/list.js';
+export * as 'admin/ad/update' from './endpoints/admin/ad/update.js';
+export * as 'admin/announcements/create' from './endpoints/admin/announcements/create.js';
+export * as 'admin/announcements/delete' from './endpoints/admin/announcements/delete.js';
+export * as 'admin/announcements/list' from './endpoints/admin/announcements/list.js';
+export * as 'admin/announcements/update' from './endpoints/admin/announcements/update.js';
+export * as 'admin/approve-user' from './endpoints/admin/approve-user.js';
+export * as 'admin/avatar-decorations/create' from './endpoints/admin/avatar-decorations/create.js';
+export * as 'admin/avatar-decorations/delete' from './endpoints/admin/avatar-decorations/delete.js';
+export * as 'admin/avatar-decorations/list' from './endpoints/admin/avatar-decorations/list.js';
+export * as 'admin/avatar-decorations/update' from './endpoints/admin/avatar-decorations/update.js';
+export * as 'admin/captcha/current' from './endpoints/admin/captcha/current.js';
+export * as 'admin/captcha/save' from './endpoints/admin/captcha/save.js';
+export * as 'admin/cw-user' from './endpoints/admin/cw-user.js';
+export * as 'admin/decline-user' from './endpoints/admin/decline-user.js';
+export * as 'admin/delete-account' from './endpoints/admin/delete-account.js';
+export * as 'admin/delete-all-files-of-a-user' from './endpoints/admin/delete-all-files-of-a-user.js';
+export * as 'admin/drive/clean-remote-files' from './endpoints/admin/drive/clean-remote-files.js';
+export * as 'admin/drive/cleanup' from './endpoints/admin/drive/cleanup.js';
+export * as 'admin/drive/files' from './endpoints/admin/drive/files.js';
+export * as 'admin/drive/show-file' from './endpoints/admin/drive/show-file.js';
+export * as 'admin/emoji/add' from './endpoints/admin/emoji/add.js';
+export * as 'admin/emoji/add-aliases-bulk' from './endpoints/admin/emoji/add-aliases-bulk.js';
+export * as 'admin/emoji/copy' from './endpoints/admin/emoji/copy.js';
+export * as 'admin/emoji/delete' from './endpoints/admin/emoji/delete.js';
+export * as 'admin/emoji/delete-bulk' from './endpoints/admin/emoji/delete-bulk.js';
+export * as 'admin/emoji/import-zip' from './endpoints/admin/emoji/import-zip.js';
+export * as 'admin/emoji/list' from './endpoints/admin/emoji/list.js';
+export * as 'admin/emoji/list-remote' from './endpoints/admin/emoji/list-remote.js';
+export * as 'admin/emoji/remove-aliases-bulk' from './endpoints/admin/emoji/remove-aliases-bulk.js';
+export * as 'admin/emoji/set-aliases-bulk' from './endpoints/admin/emoji/set-aliases-bulk.js';
+export * as 'admin/emoji/set-category-bulk' from './endpoints/admin/emoji/set-category-bulk.js';
+export * as 'admin/emoji/set-license-bulk' from './endpoints/admin/emoji/set-license-bulk.js';
+export * as 'admin/emoji/update' from './endpoints/admin/emoji/update.js';
+export * as 'admin/federation/delete-all-files' from './endpoints/admin/federation/delete-all-files.js';
+export * as 'admin/federation/refresh-remote-instance-metadata' from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
+export * as 'admin/federation/remove-all-following' from './endpoints/admin/federation/remove-all-following.js';
+export * as 'admin/federation/update-instance' from './endpoints/admin/federation/update-instance.js';
+export * as 'admin/forward-abuse-user-report' from './endpoints/admin/forward-abuse-user-report.js';
+export * as 'admin/gen-vapid-keys' from './endpoints/admin/gen-vapid-keys.js';
+export * as 'admin/get-index-stats' from './endpoints/admin/get-index-stats.js';
+export * as 'admin/get-table-stats' from './endpoints/admin/get-table-stats.js';
+export * as 'admin/get-user-ips' from './endpoints/admin/get-user-ips.js';
+export * as 'admin/invite/create' from './endpoints/admin/invite/create.js';
+export * as 'admin/invite/list' from './endpoints/admin/invite/list.js';
+export * as 'admin/meta' from './endpoints/admin/meta.js';
+export * as 'admin/nsfw-user' from './endpoints/admin/nsfw-user.js';
+export * as 'admin/promo/create' from './endpoints/admin/promo/create.js';
+export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js';
+export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js';
+export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js';
+export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js';
+export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
+export * as 'admin/reject-quotes' from './endpoints/admin/reject-quotes.js';
+export * as 'admin/relays/add' from './endpoints/admin/relays/add.js';
+export * as 'admin/relays/list' from './endpoints/admin/relays/list.js';
+export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js';
+export * as 'admin/reset-password' from './endpoints/admin/reset-password.js';
+export * as 'admin/resolve-abuse-user-report' from './endpoints/admin/resolve-abuse-user-report.js';
+export * as 'admin/roles/assign' from './endpoints/admin/roles/assign.js';
+export * as 'admin/roles/create' from './endpoints/admin/roles/create.js';
+export * as 'admin/roles/delete' from './endpoints/admin/roles/delete.js';
+export * as 'admin/roles/list' from './endpoints/admin/roles/list.js';
+export * as 'admin/roles/show' from './endpoints/admin/roles/show.js';
+export * as 'admin/roles/unassign' from './endpoints/admin/roles/unassign.js';
+export * as 'admin/roles/update' from './endpoints/admin/roles/update.js';
+export * as 'admin/roles/update-default-policies' from './endpoints/admin/roles/update-default-policies.js';
+export * as 'admin/roles/users' from './endpoints/admin/roles/users.js';
+export * as 'admin/send-email' from './endpoints/admin/send-email.js';
+export * as 'admin/server-info' from './endpoints/admin/server-info.js';
+export * as 'admin/show-moderation-logs' from './endpoints/admin/show-moderation-logs.js';
+export * as 'admin/show-user' from './endpoints/admin/show-user.js';
+export * as 'admin/show-users' from './endpoints/admin/show-users.js';
+export * as 'admin/silence-user' from './endpoints/admin/silence-user.js';
+export * as 'admin/suspend-user' from './endpoints/admin/suspend-user.js';
+export * as 'admin/system-webhook/create' from './endpoints/admin/system-webhook/create.js';
+export * as 'admin/system-webhook/delete' from './endpoints/admin/system-webhook/delete.js';
+export * as 'admin/system-webhook/list' from './endpoints/admin/system-webhook/list.js';
+export * as 'admin/system-webhook/show' from './endpoints/admin/system-webhook/show.js';
+export * as 'admin/system-webhook/test' from './endpoints/admin/system-webhook/test.js';
+export * as 'admin/system-webhook/update' from './endpoints/admin/system-webhook/update.js';
+export * as 'admin/unnsfw-user' from './endpoints/admin/unnsfw-user.js';
+export * as 'admin/unset-user-avatar' from './endpoints/admin/unset-user-avatar.js';
+export * as 'admin/unset-user-banner' from './endpoints/admin/unset-user-banner.js';
+export * as 'admin/unsilence-user' from './endpoints/admin/unsilence-user.js';
+export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js';
+export * as 'admin/update-abuse-user-report' from './endpoints/admin/update-abuse-user-report.js';
+export * as 'admin/update-meta' from './endpoints/admin/update-meta.js';
+export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js';
+export * as 'announcements' from './endpoints/announcements.js';
+export * as 'announcements/show' from './endpoints/announcements/show.js';
+export * as 'antennas/create' from './endpoints/antennas/create.js';
+export * as 'antennas/delete' from './endpoints/antennas/delete.js';
+export * as 'antennas/list' from './endpoints/antennas/list.js';
+export * as 'antennas/notes' from './endpoints/antennas/notes.js';
+export * as 'antennas/show' from './endpoints/antennas/show.js';
+export * as 'antennas/update' from './endpoints/antennas/update.js';
+export * as 'ap/get' from './endpoints/ap/get.js';
+export * as 'ap/show' from './endpoints/ap/show.js';
+export * as 'app/create' from './endpoints/app/create.js';
+export * as 'app/show' from './endpoints/app/show.js';
+export * as 'auth/accept' from './endpoints/auth/accept.js';
+export * as 'auth/session/generate' from './endpoints/auth/session/generate.js';
+export * as 'auth/session/show' from './endpoints/auth/session/show.js';
+export * as 'auth/session/userkey' from './endpoints/auth/session/userkey.js';
+export * as 'blocking/create' from './endpoints/blocking/create.js';
+export * as 'blocking/delete' from './endpoints/blocking/delete.js';
+export * as 'blocking/list' from './endpoints/blocking/list.js';
+export * as 'bubble-game/ranking' from './endpoints/bubble-game/ranking.js';
+export * as 'bubble-game/register' from './endpoints/bubble-game/register.js';
+export * as 'channels/create' from './endpoints/channels/create.js';
+export * as 'channels/favorite' from './endpoints/channels/favorite.js';
+export * as 'channels/featured' from './endpoints/channels/featured.js';
+export * as 'channels/follow' from './endpoints/channels/follow.js';
+export * as 'channels/followed' from './endpoints/channels/followed.js';
+export * as 'channels/my-favorites' from './endpoints/channels/my-favorites.js';
+export * as 'channels/owned' from './endpoints/channels/owned.js';
+export * as 'channels/search' from './endpoints/channels/search.js';
+export * as 'channels/show' from './endpoints/channels/show.js';
+export * as 'channels/timeline' from './endpoints/channels/timeline.js';
+export * as 'channels/unfavorite' from './endpoints/channels/unfavorite.js';
+export * as 'channels/unfollow' from './endpoints/channels/unfollow.js';
+export * as 'channels/update' from './endpoints/channels/update.js';
+export * as 'charts/active-users' from './endpoints/charts/active-users.js';
+export * as 'charts/ap-request' from './endpoints/charts/ap-request.js';
+export * as 'charts/drive' from './endpoints/charts/drive.js';
+export * as 'charts/federation' from './endpoints/charts/federation.js';
+export * as 'charts/instance' from './endpoints/charts/instance.js';
+export * as 'charts/notes' from './endpoints/charts/notes.js';
+export * as 'charts/user/drive' from './endpoints/charts/user/drive.js';
+export * as 'charts/user/following' from './endpoints/charts/user/following.js';
+export * as 'charts/user/notes' from './endpoints/charts/user/notes.js';
+export * as 'charts/user/pv' from './endpoints/charts/user/pv.js';
+export * as 'charts/user/reactions' from './endpoints/charts/user/reactions.js';
+export * as 'charts/users' from './endpoints/charts/users.js';
+export * as 'clips/add-note' from './endpoints/clips/add-note.js';
+export * as 'clips/create' from './endpoints/clips/create.js';
+export * as 'clips/delete' from './endpoints/clips/delete.js';
+export * as 'clips/favorite' from './endpoints/clips/favorite.js';
+export * as 'clips/list' from './endpoints/clips/list.js';
+export * as 'clips/my-favorites' from './endpoints/clips/my-favorites.js';
+export * as 'clips/notes' from './endpoints/clips/notes.js';
+export * as 'clips/remove-note' from './endpoints/clips/remove-note.js';
+export * as 'clips/show' from './endpoints/clips/show.js';
+export * as 'clips/unfavorite' from './endpoints/clips/unfavorite.js';
+export * as 'clips/update' from './endpoints/clips/update.js';
+export * as 'drive' from './endpoints/drive.js';
+export * as 'drive/files' from './endpoints/drive/files.js';
+export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js';
+export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js';
+export * as 'drive/files/create' from './endpoints/drive/files/create.js';
+export * as 'drive/files/delete' from './endpoints/drive/files/delete.js';
+export * as 'drive/files/find' from './endpoints/drive/files/find.js';
+export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js';
+export * as 'drive/files/show' from './endpoints/drive/files/show.js';
+export * as 'drive/files/update' from './endpoints/drive/files/update.js';
+export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js';
+export * as 'drive/folders' from './endpoints/drive/folders.js';
+export * as 'drive/folders/create' from './endpoints/drive/folders/create.js';
+export * as 'drive/folders/delete' from './endpoints/drive/folders/delete.js';
+export * as 'drive/folders/find' from './endpoints/drive/folders/find.js';
+export * as 'drive/folders/show' from './endpoints/drive/folders/show.js';
+export * as 'drive/folders/update' from './endpoints/drive/folders/update.js';
+export * as 'drive/stream' from './endpoints/drive/stream.js';
+export * as 'email-address/available' from './endpoints/email-address/available.js';
+export * as 'emoji' from './endpoints/emoji.js';
+export * as 'emojis' from './endpoints/emojis.js';
+export * as 'endpoint' from './endpoints/endpoint.js';
+export * as 'endpoints' from './endpoints/endpoints.js';
+export * as 'export-custom-emojis' from './endpoints/export-custom-emojis.js';
+export * as 'federation/followers' from './endpoints/federation/followers.js';
+export * as 'federation/following' from './endpoints/federation/following.js';
+export * as 'federation/instances' from './endpoints/federation/instances.js';
+export * as 'federation/show-instance' from './endpoints/federation/show-instance.js';
+export * as 'federation/stats' from './endpoints/federation/stats.js';
+export * as 'federation/update-remote-user' from './endpoints/federation/update-remote-user.js';
+export * as 'federation/users' from './endpoints/federation/users.js';
+export * as 'fetch-external-resources' from './endpoints/fetch-external-resources.js';
+export * as 'fetch-rss' from './endpoints/fetch-rss.js';
+export * as 'flash/create' from './endpoints/flash/create.js';
+export * as 'flash/delete' from './endpoints/flash/delete.js';
+export * as 'flash/featured' from './endpoints/flash/featured.js';
+export * as 'flash/like' from './endpoints/flash/like.js';
+export * as 'flash/my' from './endpoints/flash/my.js';
+export * as 'flash/my-likes' from './endpoints/flash/my-likes.js';
+export * as 'flash/show' from './endpoints/flash/show.js';
+export * as 'flash/unlike' from './endpoints/flash/unlike.js';
+export * as 'flash/update' from './endpoints/flash/update.js';
+export * as 'following/create' from './endpoints/following/create.js';
+export * as 'following/delete' from './endpoints/following/delete.js';
+export * as 'following/invalidate' from './endpoints/following/invalidate.js';
+export * as 'following/requests/accept' from './endpoints/following/requests/accept.js';
+export * as 'following/requests/cancel' from './endpoints/following/requests/cancel.js';
+export * as 'following/requests/list' from './endpoints/following/requests/list.js';
+export * as 'following/requests/reject' from './endpoints/following/requests/reject.js';
+export * as 'following/requests/sent' from './endpoints/following/requests/sent.js';
+export * as 'following/update' from './endpoints/following/update.js';
+export * as 'following/update-all' from './endpoints/following/update-all.js';
+export * as 'gallery/featured' from './endpoints/gallery/featured.js';
+export * as 'gallery/popular' from './endpoints/gallery/popular.js';
+export * as 'gallery/posts' from './endpoints/gallery/posts.js';
+export * as 'gallery/posts/create' from './endpoints/gallery/posts/create.js';
+export * as 'gallery/posts/delete' from './endpoints/gallery/posts/delete.js';
+export * as 'gallery/posts/like' from './endpoints/gallery/posts/like.js';
+export * as 'gallery/posts/show' from './endpoints/gallery/posts/show.js';
+export * as 'gallery/posts/unlike' from './endpoints/gallery/posts/unlike.js';
+export * as 'gallery/posts/update' from './endpoints/gallery/posts/update.js';
+export * as 'get-avatar-decorations' from './endpoints/get-avatar-decorations.js';
+export * as 'get-online-users-count' from './endpoints/get-online-users-count.js';
+export * as 'hashtags/list' from './endpoints/hashtags/list.js';
+export * as 'hashtags/search' from './endpoints/hashtags/search.js';
+export * as 'hashtags/show' from './endpoints/hashtags/show.js';
+export * as 'hashtags/trend' from './endpoints/hashtags/trend.js';
+export * as 'hashtags/users' from './endpoints/hashtags/users.js';
+export * as 'i' from './endpoints/i.js';
+export * as 'i/2fa/done' from './endpoints/i/2fa/done.js';
+export * as 'i/2fa/key-done' from './endpoints/i/2fa/key-done.js';
+export * as 'i/2fa/password-less' from './endpoints/i/2fa/password-less.js';
+export * as 'i/2fa/register' from './endpoints/i/2fa/register.js';
+export * as 'i/2fa/register-key' from './endpoints/i/2fa/register-key.js';
+export * as 'i/2fa/remove-key' from './endpoints/i/2fa/remove-key.js';
+export * as 'i/2fa/unregister' from './endpoints/i/2fa/unregister.js';
+export * as 'i/2fa/update-key' from './endpoints/i/2fa/update-key.js';
+export * as 'i/apps' from './endpoints/i/apps.js';
+export * as 'i/authorized-apps' from './endpoints/i/authorized-apps.js';
+export * as 'i/change-password' from './endpoints/i/change-password.js';
+export * as 'i/claim-achievement' from './endpoints/i/claim-achievement.js';
+export * as 'i/delete-account' from './endpoints/i/delete-account.js';
+export * as 'i/export-antennas' from './endpoints/i/export-antennas.js';
+export * as 'i/export-blocking' from './endpoints/i/export-blocking.js';
+export * as 'i/export-clips' from './endpoints/i/export-clips.js';
+export * as 'i/export-data' from './endpoints/i/export-data.js';
+export * as 'i/export-favorites' from './endpoints/i/export-favorites.js';
+export * as 'i/export-following' from './endpoints/i/export-following.js';
+export * as 'i/export-mute' from './endpoints/i/export-mute.js';
+export * as 'i/export-notes' from './endpoints/i/export-notes.js';
+export * as 'i/export-user-lists' from './endpoints/i/export-user-lists.js';
+export * as 'i/favorites' from './endpoints/i/favorites.js';
+export * as 'i/gallery/likes' from './endpoints/i/gallery/likes.js';
+export * as 'i/gallery/posts' from './endpoints/i/gallery/posts.js';
+export * as 'i/import-antennas' from './endpoints/i/import-antennas.js';
+export * as 'i/import-blocking' from './endpoints/i/import-blocking.js';
+export * as 'i/import-following' from './endpoints/i/import-following.js';
+export * as 'i/import-muting' from './endpoints/i/import-muting.js';
+export * as 'i/import-notes' from './endpoints/i/import-notes.js';
+export * as 'i/import-user-lists' from './endpoints/i/import-user-lists.js';
+export * as 'i/move' from './endpoints/i/move.js';
+export * as 'i/notifications' from './endpoints/i/notifications.js';
+export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.js';
+export * as 'i/page-likes' from './endpoints/i/page-likes.js';
+export * as 'i/pages' from './endpoints/i/pages.js';
+export * as 'i/pin' from './endpoints/i/pin.js';
+export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js';
+export * as 'i/read-announcement' from './endpoints/i/read-announcement.js';
+export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js';
+export * as 'i/registry/get' from './endpoints/i/registry/get.js';
+export * as 'i/registry/get-all' from './endpoints/i/registry/get-all.js';
+export * as 'i/registry/get-detail' from './endpoints/i/registry/get-detail.js';
+export * as 'i/registry/get-unsecure' from './endpoints/i/registry/get-unsecure.js';
+export * as 'i/registry/keys' from './endpoints/i/registry/keys.js';
+export * as 'i/registry/keys-with-type' from './endpoints/i/registry/keys-with-type.js';
+export * as 'i/registry/remove' from './endpoints/i/registry/remove.js';
+export * as 'i/registry/scopes-with-domain' from './endpoints/i/registry/scopes-with-domain.js';
+export * as 'i/registry/set' from './endpoints/i/registry/set.js';
+export * as 'i/revoke-token' from './endpoints/i/revoke-token.js';
+export * as 'i/signin-history' from './endpoints/i/signin-history.js';
+export * as 'i/unpin' from './endpoints/i/unpin.js';
+export * as 'i/update' from './endpoints/i/update.js';
+export * as 'i/update-email' from './endpoints/i/update-email.js';
+export * as 'i/webhooks/create' from './endpoints/i/webhooks/create.js';
+export * as 'i/webhooks/delete' from './endpoints/i/webhooks/delete.js';
+export * as 'i/webhooks/list' from './endpoints/i/webhooks/list.js';
+export * as 'i/webhooks/show' from './endpoints/i/webhooks/show.js';
+export * as 'i/webhooks/test' from './endpoints/i/webhooks/test.js';
+export * as 'i/webhooks/update' from './endpoints/i/webhooks/update.js';
+export * as 'invite/create' from './endpoints/invite/create.js';
+export * as 'invite/delete' from './endpoints/invite/delete.js';
+export * as 'invite/limit' from './endpoints/invite/limit.js';
+export * as 'invite/list' from './endpoints/invite/list.js';
+export * as 'meta' from './endpoints/meta.js';
+export * as 'miauth/gen-token' from './endpoints/miauth/gen-token.js';
+export * as 'mute/create' from './endpoints/mute/create.js';
+export * as 'mute/delete' from './endpoints/mute/delete.js';
+export * as 'mute/list' from './endpoints/mute/list.js';
+export * as 'my/apps' from './endpoints/my/apps.js';
+export * as 'notes' from './endpoints/notes.js';
+export * as 'notes/bubble-timeline' from './endpoints/notes/bubble-timeline.js';
+export * as 'notes/children' from './endpoints/notes/children.js';
+export * as 'notes/clips' from './endpoints/notes/clips.js';
+export * as 'notes/conversation' from './endpoints/notes/conversation.js';
+export * as 'notes/create' from './endpoints/notes/create.js';
+export * as 'notes/delete' from './endpoints/notes/delete.js';
+export * as 'notes/edit' from './endpoints/notes/edit.js';
+export * as 'notes/favorites/create' from './endpoints/notes/favorites/create.js';
+export * as 'notes/favorites/delete' from './endpoints/notes/favorites/delete.js';
+export * as 'notes/featured' from './endpoints/notes/featured.js';
+export * as 'notes/following' from './endpoints/notes/following.js';
+export * as 'notes/global-timeline' from './endpoints/notes/global-timeline.js';
+export * as 'notes/hybrid-timeline' from './endpoints/notes/hybrid-timeline.js';
+export * as 'notes/like' from './endpoints/notes/like.js';
+export * as 'notes/local-timeline' from './endpoints/notes/local-timeline.js';
+export * as 'notes/mentions' from './endpoints/notes/mentions.js';
+export * as 'notes/polls/recommendation' from './endpoints/notes/polls/recommendation.js';
+export * as 'notes/polls/refresh' from './endpoints/notes/polls/refresh.js';
+export * as 'notes/polls/vote' from './endpoints/notes/polls/vote.js';
+export * as 'notes/reactions' from './endpoints/notes/reactions.js';
+export * as 'notes/reactions/create' from './endpoints/notes/reactions/create.js';
+export * as 'notes/reactions/delete' from './endpoints/notes/reactions/delete.js';
+export * as 'notes/renotes' from './endpoints/notes/renotes.js';
+export * as 'notes/replies' from './endpoints/notes/replies.js';
+export * as 'notes/search' from './endpoints/notes/search.js';
+export * as 'notes/schedule/create' from './endpoints/notes/schedule/create.js';
+export * as 'notes/schedule/delete' from './endpoints/notes/schedule/delete.js';
+export * as 'notes/schedule/list' from './endpoints/notes/schedule/list.js';
+export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js';
+export * as 'notes/show' from './endpoints/notes/show.js';
+export * as 'notes/state' from './endpoints/notes/state.js';
+export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js';
+export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js';
+export * as 'notes/timeline' from './endpoints/notes/timeline.js';
+export * as 'notes/translate' from './endpoints/notes/translate.js';
+export * as 'notes/unrenote' from './endpoints/notes/unrenote.js';
+export * as 'notes/user-list-timeline' from './endpoints/notes/user-list-timeline.js';
+export * as 'notes/versions' from './endpoints/notes/versions.js';
+export * as 'notifications/create' from './endpoints/notifications/create.js';
+export * as 'notifications/flush' from './endpoints/notifications/flush.js';
+export * as 'notifications/mark-all-as-read' from './endpoints/notifications/mark-all-as-read.js';
+export * as 'notifications/test-notification' from './endpoints/notifications/test-notification.js';
+export * as 'page-push' from './endpoints/page-push.js';
+export * as 'pages/create' from './endpoints/pages/create.js';
+export * as 'pages/delete' from './endpoints/pages/delete.js';
+export * as 'pages/featured' from './endpoints/pages/featured.js';
+export * as 'pages/like' from './endpoints/pages/like.js';
+export * as 'pages/show' from './endpoints/pages/show.js';
+export * as 'pages/unlike' from './endpoints/pages/unlike.js';
+export * as 'pages/update' from './endpoints/pages/update.js';
+export * as 'ping' from './endpoints/ping.js';
+export * as 'pinned-users' from './endpoints/pinned-users.js';
+export * as 'promo/read' from './endpoints/promo/read.js';
+export * as 'renote-mute/create' from './endpoints/renote-mute/create.js';
+export * as 'renote-mute/delete' from './endpoints/renote-mute/delete.js';
+export * as 'renote-mute/list' from './endpoints/renote-mute/list.js';
+export * as 'request-reset-password' from './endpoints/request-reset-password.js';
+export * as 'reset-db' from './endpoints/reset-db.js';
+export * as 'reset-password' from './endpoints/reset-password.js';
+export * as 'retention' from './endpoints/retention.js';
+export * as 'reversi/cancel-match' from './endpoints/reversi/cancel-match.js';
+export * as 'reversi/games' from './endpoints/reversi/games.js';
+export * as 'reversi/invitations' from './endpoints/reversi/invitations.js';
+export * as 'reversi/match' from './endpoints/reversi/match.js';
+export * as 'reversi/show-game' from './endpoints/reversi/show-game.js';
+export * as 'reversi/surrender' from './endpoints/reversi/surrender.js';
+export * as 'reversi/verify' from './endpoints/reversi/verify.js';
+export * as 'roles/list' from './endpoints/roles/list.js';
+export * as 'roles/notes' from './endpoints/roles/notes.js';
+export * as 'roles/show' from './endpoints/roles/show.js';
+export * as 'roles/users' from './endpoints/roles/users.js';
+export * as 'server-info' from './endpoints/server-info.js';
+export * as 'sponsors' from './endpoints/sponsors.js';
+export * as 'stats' from './endpoints/stats.js';
+export * as 'sw/register' from './endpoints/sw/register.js';
+export * as 'sw/show-registration' from './endpoints/sw/show-registration.js';
+export * as 'sw/unregister' from './endpoints/sw/unregister.js';
+export * as 'sw/update-registration' from './endpoints/sw/update-registration.js';
+export * as 'test' from './endpoints/test.js';
+export * as 'username/available' from './endpoints/username/available.js';
+export * as 'users' from './endpoints/users.js';
+export * as 'users/achievements' from './endpoints/users/achievements.js';
+export * as 'users/clips' from './endpoints/users/clips.js';
+export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
+export * as 'users/flashs' from './endpoints/users/flashs.js';
+export * as 'users/followers' from './endpoints/users/followers.js';
+export * as 'users/following' from './endpoints/users/following.js';
+export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
+export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
+export * as 'users/lists/create' from './endpoints/users/lists/create.js';
+export * as 'users/lists/create-from-public' from './endpoints/users/lists/create-from-public.js';
+export * as 'users/lists/delete' from './endpoints/users/lists/delete.js';
+export * as 'users/lists/favorite' from './endpoints/users/lists/favorite.js';
+export * as 'users/lists/get-memberships' from './endpoints/users/lists/get-memberships.js';
+export * as 'users/lists/list' from './endpoints/users/lists/list.js';
+export * as 'users/lists/pull' from './endpoints/users/lists/pull.js';
+export * as 'users/lists/push' from './endpoints/users/lists/push.js';
+export * as 'users/lists/show' from './endpoints/users/lists/show.js';
+export * as 'users/lists/unfavorite' from './endpoints/users/lists/unfavorite.js';
+export * as 'users/lists/update' from './endpoints/users/lists/update.js';
+export * as 'users/lists/update-membership' from './endpoints/users/lists/update-membership.js';
+export * as 'users/notes' from './endpoints/users/notes.js';
+export * as 'users/pages' from './endpoints/users/pages.js';
+export * as 'users/reactions' from './endpoints/users/reactions.js';
+export * as 'users/recommendation' from './endpoints/users/recommendation.js';
+export * as 'users/relation' from './endpoints/users/relation.js';
+export * as 'users/report-abuse' from './endpoints/users/report-abuse.js';
+export * as 'users/search' from './endpoints/users/search.js';
+export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
+export * as 'users/show' from './endpoints/users/show.js';
+export * as 'users/update-memo' from './endpoints/users/update-memo.js';
+export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index b4f36234f0..fd6b9bb14b 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -7,821 +7,7 @@ import { permissions } from 'misskey-js';
import type { KeyOf, Schema } from '@/misc/json-schema.js';
import type { RateLimit } from '@/misc/rate-limit-utils.js';
-import * as ep___admin_abuseReport_notificationRecipient_list
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
-import * as ep___admin_abuseReport_notificationRecipient_show
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
-import * as ep___admin_abuseReport_notificationRecipient_create
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
-import * as ep___admin_abuseReport_notificationRecipient_update
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
-import * as ep___admin_abuseReport_notificationRecipient_delete
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
-import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
-import * as ep___admin_meta from './endpoints/admin/meta.js';
-import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
-import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
-import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
-import * as ep___admin_ad_create from './endpoints/admin/ad/create.js';
-import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js';
-import * as ep___admin_ad_list from './endpoints/admin/ad/list.js';
-import * as ep___admin_ad_update from './endpoints/admin/ad/update.js';
-import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js';
-import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
-import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
-import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
-import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
-import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
-import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
-import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
-import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
-import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
-import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
-import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
-import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
-import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
-import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js';
-import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js';
-import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js';
-import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js';
-import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js';
-import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
-import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js';
-import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js';
-import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
-import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
-import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
-import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
-import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
-import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
-import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
-import * as ep___admin_federation_refreshRemoteInstanceMetadata
- from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
-import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
-import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
-import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
-import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
-import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
-import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
-import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
-import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
-import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
-import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
-import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js';
-import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js';
-import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js';
-import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
-import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
-import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
-import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
-import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
-import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
-import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
-import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
-import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
-import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
-import * as ep___admin_showUser from './endpoints/admin/show-user.js';
-import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
-import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js';
-import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
-import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
-import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
-import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
-import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
-import * as ep___admin_declineUser from './endpoints/admin/decline-user.js';
-import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
-import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
-import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
-import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
-import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
-import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
-import * as ep___admin_roles_list from './endpoints/admin/roles/list.js';
-import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
-import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
-import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
-import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
-import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
-import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
-import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
-import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
-import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
-import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
-import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
-import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
-import * as ep___announcements from './endpoints/announcements.js';
-import * as ep___announcements_show from './endpoints/announcements/show.js';
-import * as ep___antennas_create from './endpoints/antennas/create.js';
-import * as ep___antennas_delete from './endpoints/antennas/delete.js';
-import * as ep___antennas_list from './endpoints/antennas/list.js';
-import * as ep___antennas_notes from './endpoints/antennas/notes.js';
-import * as ep___antennas_show from './endpoints/antennas/show.js';
-import * as ep___antennas_update from './endpoints/antennas/update.js';
-import * as ep___ap_get from './endpoints/ap/get.js';
-import * as ep___ap_show from './endpoints/ap/show.js';
-import * as ep___app_create from './endpoints/app/create.js';
-import * as ep___app_show from './endpoints/app/show.js';
-import * as ep___auth_accept from './endpoints/auth/accept.js';
-import * as ep___auth_session_generate from './endpoints/auth/session/generate.js';
-import * as ep___auth_session_show from './endpoints/auth/session/show.js';
-import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js';
-import * as ep___blocking_create from './endpoints/blocking/create.js';
-import * as ep___blocking_delete from './endpoints/blocking/delete.js';
-import * as ep___blocking_list from './endpoints/blocking/list.js';
-import * as ep___channels_create from './endpoints/channels/create.js';
-import * as ep___channels_featured from './endpoints/channels/featured.js';
-import * as ep___channels_follow from './endpoints/channels/follow.js';
-import * as ep___channels_followed from './endpoints/channels/followed.js';
-import * as ep___channels_owned from './endpoints/channels/owned.js';
-import * as ep___channels_show from './endpoints/channels/show.js';
-import * as ep___channels_timeline from './endpoints/channels/timeline.js';
-import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
-import * as ep___channels_update from './endpoints/channels/update.js';
-import * as ep___channels_favorite from './endpoints/channels/favorite.js';
-import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
-import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
-import * as ep___channels_search from './endpoints/channels/search.js';
-import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
-import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
-import * as ep___charts_drive from './endpoints/charts/drive.js';
-import * as ep___charts_federation from './endpoints/charts/federation.js';
-import * as ep___charts_instance from './endpoints/charts/instance.js';
-import * as ep___charts_notes from './endpoints/charts/notes.js';
-import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
-import * as ep___charts_user_following from './endpoints/charts/user/following.js';
-import * as ep___charts_user_notes from './endpoints/charts/user/notes.js';
-import * as ep___charts_user_pv from './endpoints/charts/user/pv.js';
-import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js';
-import * as ep___charts_users from './endpoints/charts/users.js';
-import * as ep___clips_addNote from './endpoints/clips/add-note.js';
-import * as ep___clips_removeNote from './endpoints/clips/remove-note.js';
-import * as ep___clips_create from './endpoints/clips/create.js';
-import * as ep___clips_delete from './endpoints/clips/delete.js';
-import * as ep___clips_list from './endpoints/clips/list.js';
-import * as ep___clips_notes from './endpoints/clips/notes.js';
-import * as ep___clips_show from './endpoints/clips/show.js';
-import * as ep___clips_update from './endpoints/clips/update.js';
-import * as ep___clips_favorite from './endpoints/clips/favorite.js';
-import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
-import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
-import * as ep___drive from './endpoints/drive.js';
-import * as ep___drive_files from './endpoints/drive/files.js';
-import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
-import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js';
-import * as ep___drive_files_create from './endpoints/drive/files/create.js';
-import * as ep___drive_files_delete from './endpoints/drive/files/delete.js';
-import * as ep___drive_files_findByHash from './endpoints/drive/files/find-by-hash.js';
-import * as ep___drive_files_find from './endpoints/drive/files/find.js';
-import * as ep___drive_files_show from './endpoints/drive/files/show.js';
-import * as ep___drive_files_update from './endpoints/drive/files/update.js';
-import * as ep___drive_files_uploadFromUrl from './endpoints/drive/files/upload-from-url.js';
-import * as ep___drive_folders from './endpoints/drive/folders.js';
-import * as ep___drive_folders_create from './endpoints/drive/folders/create.js';
-import * as ep___drive_folders_delete from './endpoints/drive/folders/delete.js';
-import * as ep___drive_folders_find from './endpoints/drive/folders/find.js';
-import * as ep___drive_folders_show from './endpoints/drive/folders/show.js';
-import * as ep___drive_folders_update from './endpoints/drive/folders/update.js';
-import * as ep___drive_stream from './endpoints/drive/stream.js';
-import * as ep___emailAddress_available from './endpoints/email-address/available.js';
-import * as ep___endpoint from './endpoints/endpoint.js';
-import * as ep___endpoints from './endpoints/endpoints.js';
-import * as ep___exportCustomEmojis from './endpoints/export-custom-emojis.js';
-import * as ep___federation_followers from './endpoints/federation/followers.js';
-import * as ep___federation_following from './endpoints/federation/following.js';
-import * as ep___federation_instances from './endpoints/federation/instances.js';
-import * as ep___federation_showInstance from './endpoints/federation/show-instance.js';
-import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js';
-import * as ep___federation_users from './endpoints/federation/users.js';
-import * as ep___federation_stats from './endpoints/federation/stats.js';
-import * as ep___following_create from './endpoints/following/create.js';
-import * as ep___following_delete from './endpoints/following/delete.js';
-import * as ep___following_update from './endpoints/following/update.js';
-import * as ep___following_update_all from './endpoints/following/update-all.js';
-import * as ep___following_invalidate from './endpoints/following/invalidate.js';
-import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
-import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
-import * as ep___following_requests_list from './endpoints/following/requests/list.js';
-import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
-import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
-import * as ep___gallery_featured from './endpoints/gallery/featured.js';
-import * as ep___gallery_popular from './endpoints/gallery/popular.js';
-import * as ep___gallery_posts from './endpoints/gallery/posts.js';
-import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js';
-import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js';
-import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js';
-import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
-import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
-import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
-import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
-import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
-import * as ep___hashtags_list from './endpoints/hashtags/list.js';
-import * as ep___hashtags_search from './endpoints/hashtags/search.js';
-import * as ep___hashtags_show from './endpoints/hashtags/show.js';
-import * as ep___hashtags_trend from './endpoints/hashtags/trend.js';
-import * as ep___hashtags_users from './endpoints/hashtags/users.js';
-import * as ep___i from './endpoints/i.js';
-import * as ep___i_2fa_done from './endpoints/i/2fa/done.js';
-import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
-import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
-import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
-import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
-import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
-import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
-import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
-import * as ep___i_apps from './endpoints/i/apps.js';
-import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
-import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
-import * as ep___i_changePassword from './endpoints/i/change-password.js';
-import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
-import * as ep___i_exportData from './endpoints/i/export-data.js';
-import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
-import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
-import * as ep___i_exportMute from './endpoints/i/export-mute.js';
-import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
-import * as ep___i_exportClips from './endpoints/i/export-clips.js';
-import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
-import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
-import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
-import * as ep___i_favorites from './endpoints/i/favorites.js';
-import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
-import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
-import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
-import * as ep___i_importFollowing from './endpoints/i/import-following.js';
-import * as ep___i_importNotes from './endpoints/i/import-notes.js';
-import * as ep___i_importMuting from './endpoints/i/import-muting.js';
-import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
-import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
-import * as ep___i_notifications from './endpoints/i/notifications.js';
-import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js';
-import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
-import * as ep___i_pages from './endpoints/i/pages.js';
-import * as ep___i_pin from './endpoints/i/pin.js';
-import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js';
-import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js';
-import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js';
-import * as ep___i_registry_getAll from './endpoints/i/registry/get-all.js';
-import * as ep___i_registry_getUnsecure from './endpoints/i/registry/get-unsecure.js';
-import * as ep___i_registry_getDetail from './endpoints/i/registry/get-detail.js';
-import * as ep___i_registry_get from './endpoints/i/registry/get.js';
-import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
-import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
-import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
-import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
-import * as ep___i_registry_set from './endpoints/i/registry/set.js';
-import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
-import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
-import * as ep___i_unpin from './endpoints/i/unpin.js';
-import * as ep___i_updateEmail from './endpoints/i/update-email.js';
-import * as ep___i_update from './endpoints/i/update.js';
-import * as ep___i_move from './endpoints/i/move.js';
-import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
-import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
-import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
-import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
-import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
-import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
-import * as ep___invite_create from './endpoints/invite/create.js';
-import * as ep___invite_delete from './endpoints/invite/delete.js';
-import * as ep___invite_list from './endpoints/invite/list.js';
-import * as ep___invite_limit from './endpoints/invite/limit.js';
-import * as ep___meta from './endpoints/meta.js';
-import * as ep___emojis from './endpoints/emojis.js';
-import * as ep___emoji from './endpoints/emoji.js';
-import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
-import * as ep___mute_create from './endpoints/mute/create.js';
-import * as ep___mute_delete from './endpoints/mute/delete.js';
-import * as ep___mute_list from './endpoints/mute/list.js';
-import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
-import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
-import * as ep___renoteMute_list from './endpoints/renote-mute/list.js';
-import * as ep___my_apps from './endpoints/my/apps.js';
-import * as ep___notes from './endpoints/notes.js';
-import * as ep___notes_children from './endpoints/notes/children.js';
-import * as ep___notes_clips from './endpoints/notes/clips.js';
-import * as ep___notes_conversation from './endpoints/notes/conversation.js';
-import * as ep___notes_create from './endpoints/notes/create.js';
-import * as ep___notes_delete from './endpoints/notes/delete.js';
-import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
-import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
-import * as ep___notes_featured from './endpoints/notes/featured.js';
-import * as ep___notes_following from './endpoints/notes/following.js';
-import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
-import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js';
-import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
-import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
-import * as ep___notes_mentions from './endpoints/notes/mentions.js';
-import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
-import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
-import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js';
-import * as ep___notes_reactions from './endpoints/notes/reactions.js';
-import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js';
-import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js';
-import * as ep___notes_like from './endpoints/notes/like.js';
-import * as ep___notes_renotes from './endpoints/notes/renotes.js';
-import * as ep___notes_replies from './endpoints/notes/replies.js';
-import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js';
-import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
-import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js';
-import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js';
-import * as ep___notes_search from './endpoints/notes/search.js';
-import * as ep___notes_show from './endpoints/notes/show.js';
-import * as ep___notes_state from './endpoints/notes/state.js';
-import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js';
-import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js';
-import * as ep___notes_timeline from './endpoints/notes/timeline.js';
-import * as ep___notes_translate from './endpoints/notes/translate.js';
-import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
-import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
-import * as ep___notes_edit from './endpoints/notes/edit.js';
-import * as ep___notes_versions from './endpoints/notes/versions.js';
-import * as ep___notifications_create from './endpoints/notifications/create.js';
-import * as ep___notifications_flush from './endpoints/notifications/flush.js';
-import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
-import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
-import * as ep___pagePush from './endpoints/page-push.js';
-import * as ep___pages_create from './endpoints/pages/create.js';
-import * as ep___pages_delete from './endpoints/pages/delete.js';
-import * as ep___pages_featured from './endpoints/pages/featured.js';
-import * as ep___pages_like from './endpoints/pages/like.js';
-import * as ep___pages_show from './endpoints/pages/show.js';
-import * as ep___pages_unlike from './endpoints/pages/unlike.js';
-import * as ep___pages_update from './endpoints/pages/update.js';
-import * as ep___flash_create from './endpoints/flash/create.js';
-import * as ep___flash_delete from './endpoints/flash/delete.js';
-import * as ep___flash_featured from './endpoints/flash/featured.js';
-import * as ep___flash_like from './endpoints/flash/like.js';
-import * as ep___flash_show from './endpoints/flash/show.js';
-import * as ep___flash_unlike from './endpoints/flash/unlike.js';
-import * as ep___flash_update from './endpoints/flash/update.js';
-import * as ep___flash_my from './endpoints/flash/my.js';
-import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
-import * as ep___ping from './endpoints/ping.js';
-import * as ep___pinnedUsers from './endpoints/pinned-users.js';
-import * as ep___promo_read from './endpoints/promo/read.js';
-import * as ep___roles_list from './endpoints/roles/list.js';
-import * as ep___roles_show from './endpoints/roles/show.js';
-import * as ep___roles_users from './endpoints/roles/users.js';
-import * as ep___roles_notes from './endpoints/roles/notes.js';
-import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
-import * as ep___resetDb from './endpoints/reset-db.js';
-import * as ep___resetPassword from './endpoints/reset-password.js';
-import * as ep___serverInfo from './endpoints/server-info.js';
-import * as ep___stats from './endpoints/stats.js';
-import * as ep___sw_show_registration from './endpoints/sw/show-registration.js';
-import * as ep___sw_update_registration from './endpoints/sw/update-registration.js';
-import * as ep___sw_register from './endpoints/sw/register.js';
-import * as ep___sw_unregister from './endpoints/sw/unregister.js';
-import * as ep___test from './endpoints/test.js';
-import * as ep___username_available from './endpoints/username/available.js';
-import * as ep___users from './endpoints/users.js';
-import * as ep___users_clips from './endpoints/users/clips.js';
-import * as ep___users_followers from './endpoints/users/followers.js';
-import * as ep___users_following from './endpoints/users/following.js';
-import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
-import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
-import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
-import * as ep___users_lists_create from './endpoints/users/lists/create.js';
-import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
-import * as ep___users_lists_list from './endpoints/users/lists/list.js';
-import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
-import * as ep___users_lists_push from './endpoints/users/lists/push.js';
-import * as ep___users_lists_show from './endpoints/users/lists/show.js';
-import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
-import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
-import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
-import * as ep___users_lists_update from './endpoints/users/lists/update.js';
-import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
-import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.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_flashs from './endpoints/users/flashs.js';
-import * as ep___users_reactions from './endpoints/users/reactions.js';
-import * as ep___users_recommendation from './endpoints/users/recommendation.js';
-import * as ep___users_relation from './endpoints/users/relation.js';
-import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
-import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
-import * as ep___users_search from './endpoints/users/search.js';
-import * as ep___users_show from './endpoints/users/show.js';
-import * as ep___users_achievements from './endpoints/users/achievements.js';
-import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
-import * as ep___fetchRss from './endpoints/fetch-rss.js';
-import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
-import * as ep___retention from './endpoints/retention.js';
-import * as ep___sponsors from './endpoints/sponsors.js';
-import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
-import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
-import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js';
-import * as ep___reversi_games from './endpoints/reversi/games.js';
-import * as ep___reversi_match from './endpoints/reversi/match.js';
-import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
-import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
-import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
-import * as ep___reversi_verify from './endpoints/reversi/verify.js';
-
-const eps = [
- ['admin/meta', ep___admin_meta],
- ['admin/abuse-user-reports', ep___admin_abuseUserReports],
- ['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list],
- ['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show],
- ['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create],
- ['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update],
- ['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete],
- ['admin/accounts/create', ep___admin_accounts_create],
- ['admin/accounts/delete', ep___admin_accounts_delete],
- ['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
- ['admin/ad/create', ep___admin_ad_create],
- ['admin/ad/delete', ep___admin_ad_delete],
- ['admin/ad/list', ep___admin_ad_list],
- ['admin/ad/update', ep___admin_ad_update],
- ['admin/announcements/create', ep___admin_announcements_create],
- ['admin/announcements/delete', ep___admin_announcements_delete],
- ['admin/announcements/list', ep___admin_announcements_list],
- ['admin/announcements/update', ep___admin_announcements_update],
- ['admin/avatar-decorations/create', ep___admin_avatarDecorations_create],
- ['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete],
- ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
- ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
- ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
- ['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
- ['admin/unset-user-banner', ep___admin_unsetUserBanner],
- ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
- ['admin/drive/cleanup', ep___admin_drive_cleanup],
- ['admin/drive/files', ep___admin_drive_files],
- ['admin/drive/show-file', ep___admin_drive_showFile],
- ['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk],
- ['admin/emoji/add', ep___admin_emoji_add],
- ['admin/emoji/copy', ep___admin_emoji_copy],
- ['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk],
- ['admin/emoji/delete', ep___admin_emoji_delete],
- ['admin/emoji/import-zip', ep___admin_emoji_importZip],
- ['admin/emoji/list-remote', ep___admin_emoji_listRemote],
- ['admin/emoji/list', ep___admin_emoji_list],
- ['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk],
- ['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk],
- ['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk],
- ['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
- ['admin/emoji/update', ep___admin_emoji_update],
- ['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
- ['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
- ['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
- ['admin/federation/update-instance', ep___admin_federation_updateInstance],
- ['admin/get-index-stats', ep___admin_getIndexStats],
- ['admin/get-table-stats', ep___admin_getTableStats],
- ['admin/get-user-ips', ep___admin_getUserIps],
- ['admin/invite/create', ep___admin_invite_create],
- ['admin/invite/list', ep___admin_invite_list],
- ['admin/promo/create', ep___admin_promo_create],
- ['admin/queue/clear', ep___admin_queue_clear],
- ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
- ['admin/queue/inbox-delayed', ep___admin_queue_inboxDelayed],
- ['admin/queue/promote', ep___admin_queue_promote],
- ['admin/queue/stats', ep___admin_queue_stats],
- ['admin/relays/add', ep___admin_relays_add],
- ['admin/relays/list', ep___admin_relays_list],
- ['admin/relays/remove', ep___admin_relays_remove],
- ['admin/reset-password', ep___admin_resetPassword],
- ['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
- ['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
- ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
- ['admin/send-email', ep___admin_sendEmail],
- ['admin/server-info', ep___admin_serverInfo],
- ['admin/show-moderation-logs', ep___admin_showModerationLogs],
- ['admin/show-user', ep___admin_showUser],
- ['admin/show-users', ep___admin_showUsers],
- ['admin/nsfw-user', ep___admin_nsfwUser],
- ['admin/unnsfw-user', ep___admin_unnsfwUser],
- ['admin/silence-user', ep___admin_silenceUser],
- ['admin/unsilence-user', ep___admin_unsilenceUser],
- ['admin/suspend-user', ep___admin_suspendUser],
- ['admin/approve-user', ep___admin_approveUser],
- ['admin/decline-user', ep___admin_declineUser],
- ['admin/unsuspend-user', ep___admin_unsuspendUser],
- ['admin/update-meta', ep___admin_updateMeta],
- ['admin/delete-account', ep___admin_deleteAccount],
- ['admin/update-user-note', ep___admin_updateUserNote],
- ['admin/roles/create', ep___admin_roles_create],
- ['admin/roles/delete', ep___admin_roles_delete],
- ['admin/roles/list', ep___admin_roles_list],
- ['admin/roles/show', ep___admin_roles_show],
- ['admin/roles/update', ep___admin_roles_update],
- ['admin/roles/assign', ep___admin_roles_assign],
- ['admin/roles/unassign', ep___admin_roles_unassign],
- ['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
- ['admin/roles/users', ep___admin_roles_users],
- ['admin/system-webhook/create', ep___admin_systemWebhook_create],
- ['admin/system-webhook/delete', ep___admin_systemWebhook_delete],
- ['admin/system-webhook/list', ep___admin_systemWebhook_list],
- ['admin/system-webhook/show', ep___admin_systemWebhook_show],
- ['admin/system-webhook/update', ep___admin_systemWebhook_update],
- ['admin/system-webhook/test', ep___admin_systemWebhook_test],
- ['announcements', ep___announcements],
- ['announcements/show', ep___announcements_show],
- ['antennas/create', ep___antennas_create],
- ['antennas/delete', ep___antennas_delete],
- ['antennas/list', ep___antennas_list],
- ['antennas/notes', ep___antennas_notes],
- ['antennas/show', ep___antennas_show],
- ['antennas/update', ep___antennas_update],
- ['ap/get', ep___ap_get],
- ['ap/show', ep___ap_show],
- ['app/create', ep___app_create],
- ['app/show', ep___app_show],
- ['auth/accept', ep___auth_accept],
- ['auth/session/generate', ep___auth_session_generate],
- ['auth/session/show', ep___auth_session_show],
- ['auth/session/userkey', ep___auth_session_userkey],
- ['blocking/create', ep___blocking_create],
- ['blocking/delete', ep___blocking_delete],
- ['blocking/list', ep___blocking_list],
- ['channels/create', ep___channels_create],
- ['channels/featured', ep___channels_featured],
- ['channels/follow', ep___channels_follow],
- ['channels/followed', ep___channels_followed],
- ['channels/owned', ep___channels_owned],
- ['channels/show', ep___channels_show],
- ['channels/timeline', ep___channels_timeline],
- ['channels/unfollow', ep___channels_unfollow],
- ['channels/update', ep___channels_update],
- ['channels/favorite', ep___channels_favorite],
- ['channels/unfavorite', ep___channels_unfavorite],
- ['channels/my-favorites', ep___channels_myFavorites],
- ['channels/search', ep___channels_search],
- ['charts/active-users', ep___charts_activeUsers],
- ['charts/ap-request', ep___charts_apRequest],
- ['charts/drive', ep___charts_drive],
- ['charts/federation', ep___charts_federation],
- ['charts/instance', ep___charts_instance],
- ['charts/notes', ep___charts_notes],
- ['charts/user/drive', ep___charts_user_drive],
- ['charts/user/following', ep___charts_user_following],
- ['charts/user/notes', ep___charts_user_notes],
- ['charts/user/pv', ep___charts_user_pv],
- ['charts/user/reactions', ep___charts_user_reactions],
- ['charts/users', ep___charts_users],
- ['clips/add-note', ep___clips_addNote],
- ['clips/remove-note', ep___clips_removeNote],
- ['clips/create', ep___clips_create],
- ['clips/delete', ep___clips_delete],
- ['clips/list', ep___clips_list],
- ['clips/notes', ep___clips_notes],
- ['clips/show', ep___clips_show],
- ['clips/update', ep___clips_update],
- ['clips/favorite', ep___clips_favorite],
- ['clips/unfavorite', ep___clips_unfavorite],
- ['clips/my-favorites', ep___clips_myFavorites],
- ['drive', ep___drive],
- ['drive/files', ep___drive_files],
- ['drive/files/attached-notes', ep___drive_files_attachedNotes],
- ['drive/files/check-existence', ep___drive_files_checkExistence],
- ['drive/files/create', ep___drive_files_create],
- ['drive/files/delete', ep___drive_files_delete],
- ['drive/files/find-by-hash', ep___drive_files_findByHash],
- ['drive/files/find', ep___drive_files_find],
- ['drive/files/show', ep___drive_files_show],
- ['drive/files/update', ep___drive_files_update],
- ['drive/files/upload-from-url', ep___drive_files_uploadFromUrl],
- ['drive/folders', ep___drive_folders],
- ['drive/folders/create', ep___drive_folders_create],
- ['drive/folders/delete', ep___drive_folders_delete],
- ['drive/folders/find', ep___drive_folders_find],
- ['drive/folders/show', ep___drive_folders_show],
- ['drive/folders/update', ep___drive_folders_update],
- ['drive/stream', ep___drive_stream],
- ['email-address/available', ep___emailAddress_available],
- ['endpoint', ep___endpoint],
- ['endpoints', ep___endpoints],
- ['export-custom-emojis', ep___exportCustomEmojis],
- ['federation/followers', ep___federation_followers],
- ['federation/following', ep___federation_following],
- ['federation/instances', ep___federation_instances],
- ['federation/show-instance', ep___federation_showInstance],
- ['federation/update-remote-user', ep___federation_updateRemoteUser],
- ['federation/users', ep___federation_users],
- ['federation/stats', ep___federation_stats],
- ['following/create', ep___following_create],
- ['following/delete', ep___following_delete],
- ['following/update', ep___following_update],
- ['following/update-all', ep___following_update_all],
- ['following/invalidate', ep___following_invalidate],
- ['following/requests/accept', ep___following_requests_accept],
- ['following/requests/cancel', ep___following_requests_cancel],
- ['following/requests/list', ep___following_requests_list],
- ['following/requests/sent', ep___following_requests_sent],
- ['following/requests/reject', ep___following_requests_reject],
- ['gallery/featured', ep___gallery_featured],
- ['gallery/popular', ep___gallery_popular],
- ['gallery/posts', ep___gallery_posts],
- ['gallery/posts/create', ep___gallery_posts_create],
- ['gallery/posts/delete', ep___gallery_posts_delete],
- ['gallery/posts/like', ep___gallery_posts_like],
- ['gallery/posts/show', ep___gallery_posts_show],
- ['gallery/posts/unlike', ep___gallery_posts_unlike],
- ['gallery/posts/update', ep___gallery_posts_update],
- ['get-online-users-count', ep___getOnlineUsersCount],
- ['get-avatar-decorations', ep___getAvatarDecorations],
- ['hashtags/list', ep___hashtags_list],
- ['hashtags/search', ep___hashtags_search],
- ['hashtags/show', ep___hashtags_show],
- ['hashtags/trend', ep___hashtags_trend],
- ['hashtags/users', ep___hashtags_users],
- ['i', ep___i],
- ['i/2fa/done', ep___i_2fa_done],
- ['i/2fa/key-done', ep___i_2fa_keyDone],
- ['i/2fa/password-less', ep___i_2fa_passwordLess],
- ['i/2fa/register-key', ep___i_2fa_registerKey],
- ['i/2fa/register', ep___i_2fa_register],
- ['i/2fa/update-key', ep___i_2fa_updateKey],
- ['i/2fa/remove-key', ep___i_2fa_removeKey],
- ['i/2fa/unregister', ep___i_2fa_unregister],
- ['i/apps', ep___i_apps],
- ['i/authorized-apps', ep___i_authorizedApps],
- ['i/claim-achievement', ep___i_claimAchievement],
- ['i/change-password', ep___i_changePassword],
- ['i/delete-account', ep___i_deleteAccount],
- ['i/export-data', ep___i_exportData],
- ['i/export-blocking', ep___i_exportBlocking],
- ['i/export-following', ep___i_exportFollowing],
- ['i/export-mute', ep___i_exportMute],
- ['i/export-notes', ep___i_exportNotes],
- ['i/export-clips', ep___i_exportClips],
- ['i/export-favorites', ep___i_exportFavorites],
- ['i/export-user-lists', ep___i_exportUserLists],
- ['i/export-antennas', ep___i_exportAntennas],
- ['i/favorites', ep___i_favorites],
- ['i/gallery/likes', ep___i_gallery_likes],
- ['i/gallery/posts', ep___i_gallery_posts],
- ['i/import-blocking', ep___i_importBlocking],
- ['i/import-following', ep___i_importFollowing],
- ['i/import-notes', ep___i_importNotes],
- ['i/import-muting', ep___i_importMuting],
- ['i/import-user-lists', ep___i_importUserLists],
- ['i/import-antennas', ep___i_importAntennas],
- ['i/notifications', ep___i_notifications],
- ['i/notifications-grouped', ep___i_notificationsGrouped],
- ['i/page-likes', ep___i_pageLikes],
- ['i/pages', ep___i_pages],
- ['i/pin', ep___i_pin],
- ['i/read-all-unread-notes', ep___i_readAllUnreadNotes],
- ['i/read-announcement', ep___i_readAnnouncement],
- ['i/regenerate-token', ep___i_regenerateToken],
- ['i/registry/get-all', ep___i_registry_getAll],
- ['i/registry/get-unsecure', ep___i_registry_getUnsecure],
- ['i/registry/get-detail', ep___i_registry_getDetail],
- ['i/registry/get', ep___i_registry_get],
- ['i/registry/keys-with-type', ep___i_registry_keysWithType],
- ['i/registry/keys', ep___i_registry_keys],
- ['i/registry/remove', ep___i_registry_remove],
- ['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain],
- ['i/registry/set', ep___i_registry_set],
- ['i/revoke-token', ep___i_revokeToken],
- ['i/signin-history', ep___i_signinHistory],
- ['i/unpin', ep___i_unpin],
- ['i/update-email', ep___i_updateEmail],
- ['i/update', ep___i_update],
- ['i/move', ep___i_move],
- ['i/webhooks/create', ep___i_webhooks_create],
- ['i/webhooks/list', ep___i_webhooks_list],
- ['i/webhooks/show', ep___i_webhooks_show],
- ['i/webhooks/update', ep___i_webhooks_update],
- ['i/webhooks/delete', ep___i_webhooks_delete],
- ['i/webhooks/test', ep___i_webhooks_test],
- ['invite/create', ep___invite_create],
- ['invite/delete', ep___invite_delete],
- ['invite/list', ep___invite_list],
- ['invite/limit', ep___invite_limit],
- ['meta', ep___meta],
- ['emojis', ep___emojis],
- ['emoji', ep___emoji],
- ['miauth/gen-token', ep___miauth_genToken],
- ['mute/create', ep___mute_create],
- ['mute/delete', ep___mute_delete],
- ['mute/list', ep___mute_list],
- ['renote-mute/create', ep___renoteMute_create],
- ['renote-mute/delete', ep___renoteMute_delete],
- ['renote-mute/list', ep___renoteMute_list],
- ['my/apps', ep___my_apps],
- ['notes', ep___notes],
- ['notes/children', ep___notes_children],
- ['notes/clips', ep___notes_clips],
- ['notes/conversation', ep___notes_conversation],
- ['notes/create', ep___notes_create],
- ['notes/delete', ep___notes_delete],
- ['notes/favorites/create', ep___notes_favorites_create],
- ['notes/favorites/delete', ep___notes_favorites_delete],
- ['notes/featured', ep___notes_featured],
- ['notes/following', ep___notes_following],
- ['notes/global-timeline', ep___notes_globalTimeline],
- ['notes/bubble-timeline', ep___notes_bubbleTimeline],
- ['notes/hybrid-timeline', ep___notes_hybridTimeline],
- ['notes/local-timeline', ep___notes_localTimeline],
- ['notes/mentions', ep___notes_mentions],
- ['notes/polls/recommendation', ep___notes_polls_recommendation],
- ['notes/polls/vote', ep___notes_polls_vote],
- ['notes/polls/refresh', ep___notes_polls_refresh],
- ['notes/reactions', ep___notes_reactions],
- ['notes/reactions/create', ep___notes_reactions_create],
- ['notes/reactions/delete', ep___notes_reactions_delete],
- ['notes/like', ep___notes_like],
- ['notes/renotes', ep___notes_renotes],
- ['notes/replies', ep___notes_replies],
- ['notes/schedule/create', ep___notes_schedule_create],
- ['notes/schedule/delete', ep___notes_schedule_delete],
- ['notes/schedule/list', ep___notes_schedule_list],
- ['notes/search-by-tag', ep___notes_searchByTag],
- ['notes/search', ep___notes_search],
- ['notes/show', ep___notes_show],
- ['notes/state', ep___notes_state],
- ['notes/thread-muting/create', ep___notes_threadMuting_create],
- ['notes/thread-muting/delete', ep___notes_threadMuting_delete],
- ['notes/timeline', ep___notes_timeline],
- ['notes/translate', ep___notes_translate],
- ['notes/unrenote', ep___notes_unrenote],
- ['notes/user-list-timeline', ep___notes_userListTimeline],
- ['notes/edit', ep___notes_edit],
- ['notes/versions', ep___notes_versions],
- ['notifications/create', ep___notifications_create],
- ['notifications/flush', ep___notifications_flush],
- ['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
- ['notifications/test-notification', ep___notifications_testNotification],
- ['page-push', ep___pagePush],
- ['pages/create', ep___pages_create],
- ['pages/delete', ep___pages_delete],
- ['pages/featured', ep___pages_featured],
- ['pages/like', ep___pages_like],
- ['pages/show', ep___pages_show],
- ['pages/unlike', ep___pages_unlike],
- ['pages/update', ep___pages_update],
- ['flash/create', ep___flash_create],
- ['flash/delete', ep___flash_delete],
- ['flash/featured', ep___flash_featured],
- ['flash/like', ep___flash_like],
- ['flash/show', ep___flash_show],
- ['flash/unlike', ep___flash_unlike],
- ['flash/update', ep___flash_update],
- ['flash/my', ep___flash_my],
- ['flash/my-likes', ep___flash_myLikes],
- ['ping', ep___ping],
- ['pinned-users', ep___pinnedUsers],
- ['promo/read', ep___promo_read],
- ['roles/list', ep___roles_list],
- ['roles/show', ep___roles_show],
- ['roles/users', ep___roles_users],
- ['roles/notes', ep___roles_notes],
- ['request-reset-password', ep___requestResetPassword],
- ['reset-db', ep___resetDb],
- ['reset-password', ep___resetPassword],
- ['server-info', ep___serverInfo],
- ['stats', ep___stats],
- ['sw/show-registration', ep___sw_show_registration],
- ['sw/update-registration', ep___sw_update_registration],
- ['sw/register', ep___sw_register],
- ['sw/unregister', ep___sw_unregister],
- ['test', ep___test],
- ['username/available', ep___username_available],
- ['users', ep___users],
- ['users/clips', ep___users_clips],
- ['users/followers', ep___users_followers],
- ['users/following', ep___users_following],
- ['users/gallery/posts', ep___users_gallery_posts],
- ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
- ['users/featured-notes', ep___users_featuredNotes],
- ['users/lists/create', ep___users_lists_create],
- ['users/lists/delete', ep___users_lists_delete],
- ['users/lists/list', ep___users_lists_list],
- ['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_createFromPublic],
- ['users/lists/update-membership', ep___users_lists_updateMembership],
- ['users/lists/get-memberships', ep___users_lists_getMemberships],
- ['users/notes', ep___users_notes],
- ['users/pages', ep___users_pages],
- ['users/flashs', ep___users_flashs],
- ['users/reactions', ep___users_reactions],
- ['users/recommendation', ep___users_recommendation],
- ['users/relation', ep___users_relation],
- ['users/report-abuse', ep___users_reportAbuse],
- ['users/search-by-username-and-host', ep___users_searchByUsernameAndHost],
- ['users/search', ep___users_search],
- ['users/show', ep___users_show],
- ['users/achievements', ep___users_achievements],
- ['users/update-memo', ep___users_updateMemo],
- ['fetch-rss', ep___fetchRss],
- ['fetch-external-resources', ep___fetchExternalResources],
- ['retention', ep___retention],
- ['sponsors', ep___sponsors],
- ['bubble-game/register', ep___bubbleGame_register],
- ['bubble-game/ranking', ep___bubbleGame_ranking],
- ['reversi/cancel-match', ep___reversi_cancelMatch],
- ['reversi/games', ep___reversi_games],
- ['reversi/match', ep___reversi_match],
- ['reversi/invitations', ep___reversi_invitations],
- ['reversi/show-game', ep___reversi_showGame],
- ['reversi/surrender', ep___reversi_surrender],
- ['reversi/verify', ep___reversi_verify],
-];
+import * as endpointsObject from './endpoint-list.js';
interface IEndpointMetaBase {
readonly stability?: 'deprecated' | 'experimental' | 'stable';
@@ -922,7 +108,7 @@ export interface IEndpoint {
params: Schema;
}
-const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
+const endpoints: IEndpoint[] = Object.entries(endpointsObject).map(([name, ep]) => {
return {
name: name,
get meta() {
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index 53b1c4c4ec..1a47f56bc6 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
-import { MiAccessToken, MiUser } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
@@ -16,6 +15,7 @@ import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { Packed } from '@/misc/json-schema.js';
import { RoleService } from '@/core/RoleService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
@@ -138,6 +139,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
approved: true,
});
+ if (me) {
+ await this.moderationLogService.log(me, 'createAccount', {
+ userId: account.id,
+ userUsername: account.username,
+ });
+ }
+
const res = await this.userEntityService.pack(account, account, {
schema: 'MeDetailed',
includeSecrets: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/current.ts b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts
new file mode 100644
index 0000000000..41192c1926
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
+
+export const meta = {
+ tags: ['admin', 'captcha'],
+
+ requireCredential: true,
+ requireAdmin: true,
+
+ // 実態はmetaの取得であるため
+ kind: 'read:admin:meta',
+
+ res: {
+ type: 'object',
+ properties: {
+ provider: {
+ type: 'string',
+ enum: supportedCaptchaProviders,
+ },
+ hcaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ mcaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ instanceUrl: { type: 'string', nullable: true },
+ },
+ },
+ recaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ turnstile: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ fc: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ },
+ },
+} as const;
+
+export const paramDef = {} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private captchaService: CaptchaService,
+ ) {
+ super(meta, paramDef, async () => {
+ return this.captchaService.get();
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/save.ts b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts
new file mode 100644
index 0000000000..98ec278ebe
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts
@@ -0,0 +1,129 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { captchaErrorCodes, CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['admin', 'captcha'],
+
+ requireCredential: true,
+ requireAdmin: true,
+
+ // 実態はmetaの更新であるため
+ kind: 'write:admin:meta',
+
+ errors: {
+ invalidProvider: {
+ message: 'Invalid provider.',
+ code: 'INVALID_PROVIDER',
+ id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0',
+ httpStatusCode: 400,
+ },
+ invalidParameters: {
+ message: 'Invalid parameters.',
+ code: 'INVALID_PARAMETERS',
+ id: '26654194-410e-44e2-b42e-460ff6f92476',
+ httpStatusCode: 400,
+ },
+ noResponseProvided: {
+ message: 'No response provided.',
+ code: 'NO_RESPONSE_PROVIDED',
+ id: '40acbba8-0937-41fb-bb3f-474514d40afe',
+ httpStatusCode: 400,
+ },
+ requestFailed: {
+ message: 'Request failed.',
+ code: 'REQUEST_FAILED',
+ id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd',
+ httpStatusCode: 500,
+ },
+ verificationFailed: {
+ message: 'Verification failed.',
+ code: 'VERIFICATION_FAILED',
+ id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214',
+ httpStatusCode: 400,
+ },
+ unknown: {
+ message: 'unknown',
+ code: 'UNKNOWN',
+ id: 'f868d509-e257-42a9-99c1-42614b031a97',
+ httpStatusCode: 500,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ provider: {
+ type: 'string',
+ enum: supportedCaptchaProviders,
+ },
+ captchaResult: {
+ type: 'string', nullable: true,
+ },
+ sitekey: {
+ type: 'string', nullable: true,
+ },
+ secret: {
+ type: 'string', nullable: true,
+ },
+ instanceUrl: {
+ type: 'string', nullable: true,
+ },
+ },
+ required: ['provider'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private captchaService: CaptchaService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const result = await this.captchaService.save(ps.provider, {
+ sitekey: ps.sitekey,
+ secret: ps.secret,
+ instanceUrl: ps.instanceUrl,
+ captchaResult: ps.captchaResult,
+ });
+
+ if (!result.success) {
+ switch (result.error.code) {
+ case captchaErrorCodes.invalidProvider:
+ throw new ApiError({
+ ...meta.errors.invalidProvider,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.invalidParameters:
+ throw new ApiError({
+ ...meta.errors.invalidParameters,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.noResponseProvided:
+ throw new ApiError({
+ ...meta.errors.noResponseProvided,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.requestFailed:
+ throw new ApiError({
+ ...meta.errors.requestFailed,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.verificationFailed:
+ throw new ApiError({
+ ...meta.errors.verificationFailed,
+ message: result.error.message,
+ });
+ default:
+ throw new ApiError(meta.errors.unknown);
+ }
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/cw-user.ts b/packages/backend/src/server/api/endpoints/admin/cw-user.ts
new file mode 100644
index 0000000000..bdcfa6a0d9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/cw-user.ts
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:cw-user',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ cw: { type: 'string', nullable: true },
+ },
+ required: ['userId', 'cw'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.usersRepository)
+ private readonly usersRepository: UsersRepository,
+
+ private readonly globalEventService: GlobalEventService,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
+
+ // Skip if there's nothing to do
+ if (user.mandatoryCW === ps.cw) return;
+
+ // Log event first.
+ // This ensures that we don't "lose" the log if an error occurs
+ await this.moderationLogService.log(me, 'setMandatoryCW', {
+ newCW: ps.cw,
+ oldCW: user.mandatoryCW,
+ userId: user.id,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
+ await this.usersRepository.update(ps.userId, {
+ // Collapse empty strings to null
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ mandatoryCW: ps.cw || null,
+ });
+
+ // Synchronize caches and other processes
+ this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
index 747c9f48d0..8b4a450ccb 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
@@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
export const meta = {
tags: ['admin'],
@@ -30,14 +32,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
const files = await this.driveFilesRepository.findBy({
userId: ps.userId,
});
+ await this.moderationLogService.log(me, 'clearUserFiles', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(file, false, me);
}
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
index d420a929bd..9a7b3d5d62 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -25,9 +26,11 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
- this.queueService.createCleanRemoteFilesJob();
+ await this.moderationLogService.log(me, 'clearRemoteFiles', {});
+ await this.queueService.createCleanRemoteFilesJob();
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
index d612572e2e..f5d20366cf 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
@@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -29,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -37,6 +38,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: IsNull(),
});
+ await this.moderationLogService.log(me, 'clearOwnerlessFiles', {
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(file);
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
index f4fc79bdb3..795b579041 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ addAliases: ps.aliases,
+ });
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
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 b45a3c7156..1c5316a002 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -9,6 +9,7 @@ import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { FILE_TYPE_IMAGE } from '@/const.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -24,6 +25,11 @@ export const meta = {
code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
+ unsupportedFileType: {
+ message: 'Unsupported file type.',
+ code: 'UNSUPPORTED_FILE_TYPE',
+ id: 'f7599d96-8750-af68-1633-9575d625c1a7',
+ },
duplicateName: {
message: 'Duplicate name.',
code: 'DUPLICATE_NAME',
@@ -47,15 +53,21 @@ export const paramDef = {
nullable: true,
description: 'Use `null` to reset the category.',
},
- aliases: { type: 'array', items: {
- type: 'string',
- } },
+ aliases: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
license: { type: 'string', nullable: true },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
- roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
- type: 'string',
- } },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
},
required: ['name', 'fileId'],
} as const;
@@ -67,9 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
private customEmojiService: CustomEmojiService,
-
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -78,11 +88,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
+ if (!FILE_TYPE_IMAGE.includes(driveFile.type)) throw new ApiError(meta.errors.unsupportedFileType);
if (driveFile.user !== null) await this.driveFilesRepository.update(driveFile.id, { user: null });
const emoji = await this.customEmojiService.add({
- driveFile,
+ originalUrl: driveFile.url,
+ publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+ fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: ps.category?.normalize('NFC') ?? null,
aliases: ps.aliases?.map(a => a.normalize('NFC')) ?? [],
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index acd2494131..07ffa0b1c7 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -4,7 +4,6 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { IsNull } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
@@ -88,10 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
const addedEmoji = await this.customEmojiService.add({
- driveFile,
+ originalUrl: driveFile.url,
+ publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+ fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: emoji.category?.normalize('NFC') ?? null,
- aliases: emoji.aliases?.map(a => a.normalize('NFC')),
+ aliases: emoji.aliases.map(a => a.normalize('NFC')),
host: null,
license: emoji.license,
isSensitive: emoji.isSensitive,
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
index 8e5f69c894..ee7706f31a 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
@@ -3,9 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import type { DriveFilesRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
secure: true,
@@ -25,9 +28,16 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
+ @Inject(DI.driveFilesRepository)
+ private readonly driveFilesRepository: DriveFilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
- this.queueService.createImportCustomEmojisJob(me, ps.fileId);
+ const file = await driveFilesRepository.findOneByOrFail({ id: ps.fileId });
+ await this.moderationLogService.log(me, 'importCustomEmojis', {
+ fileName: file.name,
+ });
+ await this.queueService.createImportCustomEmojisJob(me, ps.fileId);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
index e78620eac1..066eb1c7d9 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ delAliases: ps.aliases,
+ });
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
index 69fc8e0cb5..8980ef0c86 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ setAliases: ps.aliases,
+ });
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
index 679a9f9c42..2510349210 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ category: ps.category,
+ });
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category?.normalize('NFC') ?? null);
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
index 4ba99faab7..a0205ae24a 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ license: ps.license,
+ });
await this.customEmojiService.setLicenseBulk(ps.ids, ps.license ?? null);
});
}
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 071ddbef18..fd6db9c4ab 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -86,7 +86,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const error = await this.customEmojiService.update({
...required,
- driveFile,
+ originalUrl: driveFile != null ? driveFile.url : undefined,
+ publicUrl: driveFile != null ? (driveFile.webpublicUrl ?? driveFile.url) : undefined,
+ fileType: driveFile != null ? (driveFile.webpublicType ?? driveFile.type) : undefined,
category: ps.category?.normalize('NFC'),
aliases: ps.aliases?.map(a => a.normalize('NFC')),
license: ps.license,
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
index 4a54c26009..89fd4be99c 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -30,7 +31,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -38,6 +39,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userHost: ps.host,
});
+ await this.moderationLogService.log(me, 'clearInstanceFiles', {
+ host: ps.host,
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(file);
}
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
index 601c898f52..e5d85e1d57 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -35,6 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const followings = await this.followingsRepository.findBy([
@@ -51,6 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
+ await this.moderationLogService.log(me, 'severFollowRelations', {
+ host: ps.host,
+ });
+
this.queueService.createUnfollowJob(pairs.map(p => ({ from: p[0], to: p[1], silent: true })));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
index daf19c4435..24d0b8527c 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -27,6 +27,7 @@ export const paramDef = {
isNSFW: { type: 'boolean' },
rejectReports: { type: 'boolean' },
moderationNote: { type: 'string' },
+ rejectQuotes: { type: 'boolean' },
},
required: ['host'],
} as const;
@@ -59,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
suspensionState,
isNSFW: ps.isNSFW,
rejectReports: ps.rejectReports,
+ rejectQuotes: ps.rejectQuotes,
moderationNote: ps.moderationNote,
});
@@ -92,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
}
+ if (ps.rejectQuotes != null && instance.rejectQuotes !== ps.rejectQuotes) {
+ const message = ps.rejectReports ? 'rejectQuotesInstance' : 'acceptQuotesInstance';
+ this.moderationLogService.log(me, message, {
+ id: instance.id,
+ host: instance.host,
+ });
+ }
+
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
id: instance.id,
diff --git a/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts b/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts
new file mode 100644
index 0000000000..5695866265
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: marie and sharkey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import webpush from 'web-push';
+const { generateVAPIDKeys } = webpush;
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:meta',
+} as const;
+
+export const paramDef = {} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const keys = await generateVAPIDKeys();
+
+ return { public: keys.publicKey, private: keys.privateKey };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
index 5ecae3161a..e52b177e2b 100644
--- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
@@ -68,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
+ createdBy: me,
+ createdById: me.id,
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}));
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 6495e3b7da..436dcf27cb 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -391,6 +391,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ robotsTxt: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
enableIdenticonGeneration: {
type: 'boolean',
optional: false, nullable: false,
@@ -708,6 +712,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableAchievements: instance.enableAchievements,
+ robotsTxt: instance.robotsTxt,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
bannedEmailDomains: instance.bannedEmailDomains,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
diff --git a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
index d3fa4251dd..194e793eda 100644
--- a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
@@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
+import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,22 +29,25 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly cacheService: CacheService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'nsfwUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: true,
});
+
+ await this.cacheService.userProfileCache.refresh(ps.userId);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
index 1d32c6cc00..63fe2988ee 100644
--- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
@@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { PromoNotesRepository } from '@/models/_.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -46,7 +48,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.promoNotesRepository)
private promoNotesRepository: PromoNotesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
+ private readonly cacheService: CacheService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -61,6 +64,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.alreadyPromoted);
}
+ const user = await this.cacheService.findUserById(note.userId);
+ await this.moderationLogService.log(me, 'createPromo', {
+ noteId: note.id,
+ noteUserId: user.id,
+ noteUserUsername: user.username,
+ noteUserHost: user.host,
+ });
+
await this.promoNotesRepository.insert({
noteId: note.id,
expiresAt: new Date(ps.expiresAt),
diff --git a/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts b/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts
new file mode 100644
index 0000000000..78f94ceeff
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:reject-quotes',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ rejectQuotes: { type: 'boolean', nullable: false },
+ },
+ required: ['userId', 'rejectQuotes'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.usersRepository)
+ private readonly usersRepository: UsersRepository,
+
+ private readonly globalEventService: GlobalEventService,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
+
+ // Skip if there's nothing to do
+ if (user.rejectQuotes === ps.rejectQuotes) return;
+
+ // Log event first.
+ // This ensures that we don't "lose" the log if an error occurs
+ await this.moderationLogService.log(me, ps.rejectQuotes ? 'rejectQuotesUser' : 'acceptQuotesUser', {
+ userId: user.id,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
+ await this.usersRepository.update(ps.userId, {
+ rejectQuotes: ps.rejectQuotes,
+ });
+
+ // Synchronize caches and other processes
+ this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
+ });
+ }
+}
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 3d7bc4567e..129f69aca9 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js';
import { ApiError } from '../../../error.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -64,6 +65,7 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private relayService: RelayService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
try {
@@ -72,6 +74,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.invalidUrl);
}
+ await this.moderationLogService.log(me, 'addRelay', {
+ inbox: ps.inbox,
+ });
+
return await this.relayService.addRelay(ps.inbox);
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
index 1f6e773cd4..292f21fde9 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,9 +28,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private relayService: RelayService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
- return await this.relayService.removeRelay(ps.inbox);
+ await this.moderationLogService.log(me, 'removeRelay', {
+ inbox: ps.inbox,
+ });
+ await this.relayService.removeRelay(ps.inbox);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
index 7e6045049a..eed21c6576 100644
--- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
@@ -8,6 +8,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@@ -29,24 +32,32 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private roleService: RoleService,
+ private readonly usersRepository: UsersRepository,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly roleService: RoleService,
+ private readonly globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
-
- if (user == null) {
- throw new Error('user not found');
- }
+ const user = await this.cacheService.findUserById(ps.userId);
if (await this.roleService.isModerator(user)) {
throw new Error('cannot silence moderator account');
}
+ await this.moderationLogService.log(me, 'silenceUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
await this.usersRepository.update(user.id, {
isSilenced: true,
});
+
+ this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
+ id: user.id,
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
index 26588365e1..52a0c076be 100644
--- a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
@@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
+import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,18 +29,19 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'unNsfwUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: false,
diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
index f92be0d8e0..9318943785 100644
--- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
@@ -7,6 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@@ -28,18 +31,27 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
+ private readonly usersRepository: UsersRepository,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'unSilenceUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.usersRepository.update(user.id, {
isSilenced: false,
});
+
+ this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
+ id: user.id,
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 72f428d85f..b3733d3d39 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -149,6 +149,7 @@ export const paramDef = {
enableStatsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
enableAchievements: { type: 'boolean' },
+ robotsTxt: { type: 'string', nullable: true },
enableIdenticonGeneration: { type: 'boolean' },
serverRules: { type: 'array', items: { type: 'string' } },
bannedEmailDomains: { type: 'array', items: { type: 'string' } },
@@ -636,6 +637,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableAchievements = ps.enableAchievements;
}
+ if (ps.robotsTxt !== undefined) {
+ set.robotsTxt = ps.robotsTxt;
+ }
+
if (ps.enableIdenticonGeneration !== undefined) {
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
}
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index 616a77e337..22bec8ef95 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -4,11 +4,10 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNote } from '@/models/Note.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
-import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
+import { isActor, isPost, getApId, getNullableApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
@@ -18,7 +17,10 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
+import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
+import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApiError } from '../../error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
export const meta = {
tags: ['federation'],
@@ -26,12 +28,38 @@ export const meta = {
requireCredential: true,
kind: 'read:account',
+ // Up to 30 calls, then 1 per 1/2 second
limit: {
- duration: ms('1minute'),
max: 30,
+ dripRate: 500,
},
errors: {
+ federationNotAllowed: {
+ message: 'Federation for this host is not allowed.',
+ code: 'FEDERATION_NOT_ALLOWED',
+ id: '974b799e-1a29-4889-b706-18d4dd93e266',
+ },
+ uriInvalid: {
+ message: 'URI is invalid.',
+ code: 'URI_INVALID',
+ id: '1a5eab56-e47b-48c2-8d5e-217b897d70db',
+ },
+ requestFailed: {
+ message: 'Request failed.',
+ code: 'REQUEST_FAILED',
+ id: '81b539cf-4f57-4b29-bc98-032c33c0792e',
+ },
+ responseInvalid: {
+ message: 'Response from remote server is invalid.',
+ code: 'RESPONSE_INVALID',
+ id: '70193c39-54f3-4813-82f0-70a680f7495b',
+ },
+ responseInvalidIdHostNotMatch: {
+ message: 'Requested URI and response URI host does not match.',
+ code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH',
+ id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a',
+ },
noSuchObject: {
message: 'No such object.',
code: 'NO_SUCH_OBJECT',
@@ -94,6 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService,
private apNoteService: ApNoteService,
+ private readonly apRequestService: ApRequestService,
+ private readonly instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, me) => {
const object = await this.fetchAny(ps.uri, me);
@@ -110,7 +140,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
- if (!this.utilityService.isFederationAllowedUri(uri)) return null;
+ if (!this.utilityService.isFederationAllowedUri(uri)) {
+ throw new ApiError(meta.errors.federationNotAllowed);
+ }
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@@ -118,6 +150,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]));
if (local != null) return local;
+ // No local object found with that uri.
+ // Before we fetch, resolve the URI in case it has a cross-origin redirect or anything like that.
+ // Resolver.resolve() uses strict verification, which is overly paranoid for a user-provided lookup.
+ uri = await this.resolveCanonicalUri(uri); // eslint-disable-line no-param-reassign
+ if (!this.utilityService.isFederationAllowedUri(uri)) {
+ throw new ApiError(meta.errors.federationNotAllowed);
+ }
+
const host = this.utilityService.extractDbHost(uri);
// local object, not found in db? fail
@@ -125,7 +165,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver();
- const object = await resolver.resolve(uri) as any;
+ const object = await resolver.resolve(uri).catch((err) => {
+ if (err instanceof IdentifiableError) {
+ switch (err.id) {
+ // resolve
+ case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2':
+ throw new ApiError(meta.errors.uriInvalid);
+ case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5':
+ case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2':
+ throw new ApiError(meta.errors.requestFailed);
+ case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
+ throw new ApiError(meta.errors.federationNotAllowed);
+ case '72180409-793c-4973-868e-5a118eb5519b':
+ case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
+ throw new ApiError(meta.errors.responseInvalid);
+ case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
+ throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
+
+ // resolveLocal
+ case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':
+ throw new ApiError(meta.errors.uriInvalid);
+ case 'a9d946e5-d276-47f8-95fb-f04230289bb0':
+ case '06ae3170-1796-4d93-a697-2611ea6d83b6':
+ throw new ApiError(meta.errors.noSuchObject);
+ case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0':
+ throw new ApiError(meta.errors.responseInvalid);
+ }
+ }
+
+ throw new ApiError(meta.errors.requestFailed);
+ });
+
+ if (object.id == null) {
+ throw new ApiError(meta.errors.responseInvalid);
+ }
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
// これはDBに存在する可能性があるため再度DB検索
@@ -167,4 +240,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return null;
}
+
+ /**
+ * Resolves an arbitrary URI to its canonical, post-redirect form.
+ */
+ private async resolveCanonicalUri(uri: string): Promise<string> {
+ const user = await this.instanceActorService.getInstanceActor();
+ const res = await this.apRequestService.signedGet(uri, user, true);
+ return getNullableApId(res) ?? uri;
+ }
}
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index d2a75225ed..7cca688fda 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository, ChannelsRepository } from '@/models/_.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['channels'],
diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts
index 8e821da0da..5df212415d 100644
--- a/packages/backend/src/server/api/endpoints/drive/files.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
-import { Brackets } from 'typeorm';
export const meta = {
tags: ['drive'],
@@ -45,7 +45,7 @@ export const paramDef = {
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
- searchQuery: { type: 'string', default: '' }
+ searchQuery: { type: 'string', default: '' },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
index 7a009b12a1..3065bb6711 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
@@ -11,7 +11,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts
index 94a0e673a3..306a646785 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts
@@ -11,7 +11,6 @@ import { RoleService } from '@/core/RoleService.js';
import { DriveService } from '@/core/DriveService.js';
import type { Config } from '@/config.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts
index 1245706b0d..525cb8c5d6 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders.ts
@@ -42,7 +42,7 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
- searchQuery: { type: 'string', default: '' }
+ searchQuery: { type: 'string', default: '' },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index cd47c0fc68..8d51d09ea6 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -10,7 +10,6 @@ import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityServi
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts
index 3cc7f89ab9..4909c948e3 100644
--- a/packages/backend/src/server/api/endpoints/emojis.ts
+++ b/packages/backend/src/server/api/endpoints/emojis.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { EmojisRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -59,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const emojis = await this.emojisRepository.createQueryBuilder()
.where('host IS NULL')
.orderBy('LOWER(category)', 'ASC')
- .orderBy('LOWER(name)', 'ASC')
+ .addOrderBy('LOWER(name)', 'ASC')
.getMany();
return {
emojis: await this.emojiEntityService.packSimpleMany(emojis),
diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
index 3ec9522c44..5217f79065 100644
--- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
+++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
@@ -13,10 +13,11 @@ export const meta = {
requireCredential: false,
- // 2 calls per second
+ // Up to 10 calls, then 4 / second.
+ // This allows for reliable automation.
limit: {
- duration: 1000,
- max: 2,
+ max: 10,
+ dripRate: 250,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts
index 1010567113..271a44f8d5 100644
--- a/packages/backend/src/server/api/endpoints/flash/delete.ts
+++ b/packages/backend/src/server/api/endpoints/flash/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { FlashsRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['flashs'],
@@ -78,7 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
flashId: flash.id,
flashUserId: flash.userId,
flashUserUsername: user.username,
- flash,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
index 68478ba55c..9854358e3e 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['gallery'],
@@ -78,7 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
postId: post.id,
postUserId: post.userId,
postUserUsername: user.username,
- post,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts
index 9347c9ca27..48a2e3b40a 100644
--- a/packages/backend/src/server/api/endpoints/i.ts
+++ b/packages/backend/src/server/api/endpoints/i.ts
@@ -31,10 +31,12 @@ export const meta = {
},
},
- // 3 calls per second
+ // up to 20 calls, then 1 per second.
+ // This handles bursty traffic when all tabs reload as a group
limit: {
- duration: 1000,
- max: 3,
+ max: 20,
+ dripSize: 1,
+ dripRate: 1000,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
index 34b6907338..12d5ef443d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -5,12 +5,12 @@
import * as OTPAuth from 'otpauth';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
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 084d4af658..370d9915a3 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
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -14,7 +15,6 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
@@ -63,8 +63,8 @@ export const paramDef = {
required: ['password', 'name', 'credential'],
} as const;
-// eslint-disable-next-line import/no-default-export
@Injectable()
+// eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
index a1c965f603..cd520cff0f 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index 6ab50a57c9..893ea30391 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -6,13 +6,13 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index 888d0fc6ef..d27c14c69b 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -8,13 +8,13 @@ import * as argon2 from 'argon2';
import * as OTPAuth from 'otpauth';
import * as QRCode from 'qrcode';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 614fd0c498..b01e452056 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -13,7 +14,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 2773825373..2fe4fdc4c0 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/_.js';
@@ -13,7 +14,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
index eb8a63b3dc..4a41c7b984 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
@@ -5,13 +5,13 @@
//import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserSecurityKeysRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
index 661fa257a6..f290ff6844 100644
--- a/packages/backend/src/server/api/endpoints/i/apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
- permission: token.permission,
+ permission: token.app ? token.app.permission : token.permission,
})));
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index f131c7e9d1..4069683740 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -6,11 +6,11 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
index ebb25ebf1c..52f96e5bbd 100644
--- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
import type { MiMeta } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index 565eaaafc0..10fb923d4f 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -6,12 +6,12 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
index 814ffb5488..38328bb7d4 100644
--- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
@@ -6,12 +6,12 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 09c06a108d..f74452e2af 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -133,6 +133,12 @@ export const meta = {
id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
httpStatusCode: 422,
},
+
+ maxCwLength: {
+ message: 'You tried setting a default content warning which is too long.',
+ code: 'MAX_CW_LENGTH',
+ id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
+ },
},
res: {
@@ -243,6 +249,12 @@ export const paramDef = {
uniqueItems: true,
items: { type: 'string' },
},
+ defaultCW: { type: 'string', nullable: true },
+ defaultCWPriority: {
+ type: 'string',
+ enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
+ nullable: false,
+ },
},
} as const;
@@ -494,6 +506,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null;
}
+ let defaultCW = ps.defaultCW;
+ if (defaultCW !== undefined) {
+ if (defaultCW === '') defaultCW = null;
+ if (defaultCW && defaultCW.length > this.config.maxCwLength) {
+ throw new ApiError(meta.errors.maxCwLength);
+ }
+
+ profileUpdates.defaultCW = defaultCW;
+ }
+ if (ps.defaultCWPriority !== undefined) {
+ profileUpdates.defaultCWPriority = ps.defaultCWPriority;
+ }
+
//#region emojis/tags
let emojis = [] as string[];
@@ -592,7 +617,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html);
- const doc = window.document;
+ const doc: Document = window.document;
const myLink = `${this.config.url}/@${user.username}`;
diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts
index 1e14bafc87..ccc98c4b10 100644
--- a/packages/backend/src/server/api/endpoints/mute/delete.ts
+++ b/packages/backend/src/server/api/endpoints/mute/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MutingsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['account'],
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index d1cf0123dc..b0f32bfda8 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -143,6 +143,12 @@ export const meta = {
code: 'CONTAINS_TOO_MANY_MENTIONS',
id: '4de0363a-3046-481b-9b0f-feff3e211025',
},
+
+ quoteDisabledForUser: {
+ message: 'You do not have permission to create quote posts.',
+ code: 'QUOTE_DISABLED_FOR_USER',
+ id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
+ },
},
} as const;
@@ -415,6 +421,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
+ } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
+ throw new ApiError(meta.errors.quoteDisabledForUser);
}
}
throw e;
diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts
index dc94c78e75..cc2293c5d6 100644
--- a/packages/backend/src/server/api/endpoints/notes/edit.ts
+++ b/packages/backend/src/server/api/endpoints/notes/edit.ts
@@ -176,6 +176,12 @@ export const meta = {
id: '33510210-8452-094c-6227-4a6c05d99f02',
},
+ quoteDisabledForUser: {
+ message: 'You do not have permission to create quote posts.',
+ code: 'QUOTE_DISABLED_FOR_USER',
+ id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
+ },
+
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
@@ -469,6 +475,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
+ } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
+ throw new ApiError(meta.errors.quoteDisabledForUser);
}
}
throw e;
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
index 19a6a5af54..742c872d45 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
@@ -4,12 +4,12 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import type { NoteFavoritesRepository } from '@/models/_.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['notes', 'favorites'],
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 c45fcd7c5c..0f2592bd78 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -12,8 +12,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
-import { ApiError } from '../../error.js';
import { CacheService } from '@/core/CacheService.js';
+import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
diff --git a/packages/backend/src/server/api/endpoints/notes/like.ts b/packages/backend/src/server/api/endpoints/notes/like.ts
index 9068de2865..d6511faf95 100644
--- a/packages/backend/src/server/api/endpoints/notes/like.ts
+++ b/packages/backend/src/server/api/endpoints/notes/like.ts
@@ -1,5 +1,5 @@
-import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ReactionService } from '@/core/ReactionService.js';
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
index c6032fbdae..0c2e00ee8d 100644
--- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
@@ -14,12 +14,10 @@ import type {
NotesRepository,
BlockingsRepository,
DriveFilesRepository,
- ChannelsRepository,
NoteScheduleRepository,
} from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
-import type { MiChannel } from '@/models/Channel.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
@@ -210,9 +208,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
- @Inject(DI.channelsRepository)
- private channelsRepository: ChannelsRepository,
-
private queueService: QueueService,
private roleService: RoleService,
private idService: IdService,
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
index 4895733d4e..4dd3d7a81a 100644
--- a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
@@ -7,12 +7,14 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
-import type { MiNote, MiNoteSchedule, NoteScheduleRepository } from '@/models/_.js';
+import type { MiNote, MiUser, MiNoteSchedule, NoteScheduleRepository, NotesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { QueryService } from '@/core/QueryService.js';
import { Packed } from '@/misc/json-schema.js';
import { noteVisibilities } from '@/types.js';
+import { bindThis } from '@/decorators.js';
export const meta = {
tags: ['notes'],
@@ -81,7 +83,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.noteScheduleRepository)
private noteScheduleRepository: NoteScheduleRepository,
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
private userEntityService: UserEntityService,
+ private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService,
private queryService: QueryService,
) {
@@ -106,6 +112,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: string;
scheduledAt: string;
}[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => {
+ const renote = await this.fetchNote(item.note.renote, me);
+ const reply = await this.fetchNote(item.note.reply, me);
+
return {
...item,
scheduledAt: item.scheduledAt.toISOString(),
@@ -115,12 +124,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
user: user,
visibility: item.note.visibility ?? 'public',
reactionAcceptance: item.note.reactionAcceptance ?? null,
- visibleUsers: item.note.visibleUsers ? await userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
+ visibleUsers: item.note.visibleUsers ? await this.userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
fileIds: item.note.files ? item.note.files : [],
files: await this.driveFileEntityService.packManyByIds(item.note.files),
createdAt: item.scheduledAt.toISOString(),
isSchedule: true,
id: item.id,
+ renote, reply,
+ renoteId: item.note.renote,
+ replyId: item.note.reply,
},
};
}));
@@ -128,4 +140,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return scheduleNotesPack;
});
}
+
+ @bindThis
+ private async fetchNote(
+ id: MiNote['id'] | null | undefined,
+ me: MiUser,
+ ): Promise<Packed<'Note'> | null> {
+ if (id) {
+ const note = await this.notesRepository.findOneBy({ id });
+ if (note) {
+ note.reactionAndUserPairCache ??= [];
+ return this.noteEntityService.pack(note, me);
+ }
+ }
+ return null;
+ }
}
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 227ac0ebbf..6bba7bf37e 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
@@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .andWhere('note.visibility = \'public\'')
+ .andWhere("note.visibility IN ('public', 'home')") // keep in sync with NoteCreateService call to `hashtagService.updateHashtags()`
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index eca55cd085..f46f4d2adb 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { SearchService } from '@/core/SearchService.js';
+import { fileTypeCategories, SearchService } from '@/core/SearchService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
@@ -52,7 +52,11 @@ export const paramDef = {
type: 'string',
description: 'The local host is represented with `.`.',
},
- filetype: { type: 'string', nullable: true },
+ filetype: {
+ type: 'string',
+ nullable: true,
+ enum: fileTypeCategories,
+ },
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
order: { type: 'string' },
diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts
index fa03b0b457..6de5fe3d44 100644
--- a/packages/backend/src/server/api/endpoints/pages/create.ts
+++ b/packages/backend/src/server/api/endpoints/pages/create.ts
@@ -7,7 +7,7 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import type { DriveFilesRepository, PagesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
-import { MiPage } from '@/models/Page.js';
+import { MiPage, pageNameSchema } from '@/models/Page.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -51,7 +51,7 @@ export const paramDef = {
type: 'object',
properties: {
title: { type: 'string' },
- name: { type: 'string', minLength: 1 },
+ name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true },
content: { type: 'array', items: {
type: 'object', additionalProperties: true,
diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts
index c2c3215f49..c95f8ecf6b 100644
--- a/packages/backend/src/server/api/endpoints/pages/delete.ts
+++ b/packages/backend/src/server/api/endpoints/pages/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { PagesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['pages'],
@@ -79,7 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
pageId: page.id,
pageUserId: page.userId,
pageUserUsername: user.username,
- page,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index f11bbbcb1a..a6aeb6002e 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -10,6 +10,7 @@ import type { PagesRepository, DriveFilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
+import { pageNameSchema } from '@/models/Page.js';
export const meta = {
tags: ['pages'],
@@ -31,13 +32,11 @@ export const meta = {
code: 'NO_SUCH_PAGE',
id: '21149b9e-3616-4778-9592-c4ce89f5a864',
},
-
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '3c15cd52-3b4b-4274-967d-6456fc4f792b',
},
-
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
@@ -56,7 +55,7 @@ export const paramDef = {
properties: {
pageId: { type: 'string', format: 'misskey:id' },
title: { type: 'string' },
- name: { type: 'string', minLength: 1 },
+ name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true },
content: { type: 'array', items: {
type: 'object', additionalProperties: true,
@@ -102,15 +101,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
- await this.pagesRepository.findBy({
- id: Not(ps.pageId),
- userId: me.id,
- name: ps.name,
- }).then(result => {
- if (result.length > 0) {
- throw new ApiError(meta.errors.nameAlreadyExists);
- }
- });
+ if (ps.name != null) {
+ await this.pagesRepository.findBy({
+ id: Not(ps.pageId),
+ userId: me.id,
+ name: ps.name,
+ }).then(result => {
+ if (result.length > 0) {
+ throw new ApiError(meta.errors.nameAlreadyExists);
+ }
+ });
+ }
await this.pagesRepository.update(page.id, {
updatedAt: new Date(),
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index e765163f8e..528de76707 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -64,10 +64,11 @@ export const meta = {
},
},
- // 2 calls per second
+ // 24 calls, then 7 per second-ish (1 for each type of server info graph)
limit: {
- duration: 1000,
- max: 2,
+ max: 24,
+ dripSize: 7,
+ dripRate: 900,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index e659c46713..c7016d8d32 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -58,6 +58,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isInstanceMuted: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ memo: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
{
@@ -103,6 +111,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isInstanceMuted: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ memo: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
},
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index 7ebca78a7d..118362149d 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -57,10 +57,10 @@ export const meta = {
},
},
- // 5 calls per 2 seconds
+ // up to 50 calls @ 4 per second
limit: {
- duration: 1000 * 2,
- max: 5,
+ max: 50,
+ dripRate: 250,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
new file mode 100644
index 0000000000..9426318e34
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { CustomEmojiService, fetchEmojisHostTypes, fetchEmojisSortKeys } from '@/core/CustomEmojiService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireRolePolicy: 'canManageCustomEmojis',
+ kind: 'read:admin:emoji',
+
+ res: {
+ type: 'object',
+ properties: {
+ emojis: {
+ type: 'array',
+ items: {
+ type: 'object',
+ ref: 'EmojiDetailedAdmin',
+ },
+ },
+ count: { type: 'integer' },
+ allCount: { type: 'integer' },
+ allPages: { type: 'integer' },
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ query: {
+ type: 'object',
+ nullable: true,
+ properties: {
+ updatedAtFrom: { type: 'string' },
+ updatedAtTo: { type: 'string' },
+ name: { type: 'string' },
+ host: { type: 'string' },
+ uri: { type: 'string' },
+ publicUrl: { type: 'string' },
+ originalUrl: { type: 'string' },
+ type: { type: 'string' },
+ aliases: { type: 'string' },
+ category: { type: 'string' },
+ license: { type: 'string' },
+ isSensitive: { type: 'boolean' },
+ localOnly: { type: 'boolean' },
+ hostType: {
+ type: 'string',
+ enum: fetchEmojisHostTypes,
+ default: 'all',
+ },
+ roleIds: {
+ type: 'array',
+ items: { type: 'string', format: 'misskey:id' },
+ },
+ },
+ },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ page: { type: 'integer' },
+ sortKeys: {
+ type: 'array',
+ default: ['-id'],
+ items: {
+ type: 'string',
+ enum: fetchEmojisSortKeys,
+ },
+ },
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private customEmojiService: CustomEmojiService,
+ private emojiEntityService: EmojiEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const q = ps.query;
+ const result = await this.customEmojiService.fetchEmojis(
+ {
+ query: {
+ updatedAtFrom: q?.updatedAtFrom,
+ updatedAtTo: q?.updatedAtTo,
+ name: q?.name,
+ host: q?.host,
+ uri: q?.uri,
+ publicUrl: q?.publicUrl,
+ type: q?.type,
+ aliases: q?.aliases,
+ category: q?.category,
+ license: q?.license,
+ isSensitive: q?.isSensitive,
+ localOnly: q?.localOnly,
+ hostType: q?.hostType,
+ roleIds: q?.roleIds,
+ },
+ sinceId: ps.sinceId,
+ untilId: ps.untilId,
+ },
+ {
+ limit: ps.limit,
+ page: ps.page,
+ sortKeys: ps.sortKeys,
+ },
+ );
+
+ return {
+ emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis),
+ count: result.count,
+ allCount: result.allCount,
+ allPages: result.allPages,
+ };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
index b40e4cdaa4..69799bdade 100644
--- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
+++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
@@ -3,53 +3,78 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
-import megalodon, { Entity, MegalodonInterface } from 'megalodon';
import querystring from 'querystring';
+import { megalodon, Entity, MegalodonInterface } from 'megalodon';
import { IsNull } from 'typeorm';
import multer from 'fastify-multer';
-import type { AccessTokensRepository, NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository, MiMeta } from '@/models/_.js';
+import { Inject, Injectable } from '@nestjs/common';
+import type { AccessTokensRepository, UserProfilesRepository, UsersRepository, MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Config } from '@/config.js';
+import { DriveService } from '@/core/DriveService.js';
+import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
+import { ApiAccountMastodonRoute } from '@/server/api/mastodon/endpoints/account.js';
+import { ApiSearchMastodonRoute } from '@/server/api/mastodon/endpoints/search.js';
+import { ApiFilterMastodonRoute } from '@/server/api/mastodon/endpoints/filter.js';
+import { ApiNotifyMastodonRoute } from '@/server/api/mastodon/endpoints/notifications.js';
+import { AuthenticateService } from '@/server/api/AuthenticateService.js';
+import { MiLocalUser } from '@/models/User.js';
+import { AuthMastodonRoute } from './endpoints/auth.js';
+import { toBoolean } from './timelineArgs.js';
import { convertAnnouncement, convertFilter, convertAttachment, convertFeaturedTag, convertList, MastoConverters } from './converters.js';
import { getInstance } from './endpoints/meta.js';
import { ApiAuthMastodon, ApiAccountMastodon, ApiFilterMastodon, ApiNotifyMastodon, ApiSearchMastodon, ApiTimelineMastodon, ApiStatusMastodon } from './endpoints.js';
-import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { DriveService } from '@/core/DriveService.js';
+import type { FastifyInstance, FastifyPluginOptions, FastifyRequest } from 'fastify';
-export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
+export function getAccessToken(authorization: string | undefined): string | null {
const accessTokenArr = authorization?.split(' ') ?? [null];
- const accessToken = accessTokenArr[accessTokenArr.length - 1];
- const generator = (megalodon as any).default;
- const client = generator('misskey', BASE_URL, accessToken) as MegalodonInterface;
- return client;
+ return accessTokenArr[accessTokenArr.length - 1];
+}
+
+export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
+ const accessToken = getAccessToken(authorization);
+ return megalodon('misskey', BASE_URL, accessToken);
}
@Injectable()
export class MastodonApiServerService {
constructor(
@Inject(DI.meta)
- private serverSettings: MiMeta,
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
- @Inject(DI.notesRepository)
- private notesRepository: NotesRepository,
+ private readonly serverSettings: MiMeta,
+ @Inject(DI.usersRepository)
+ private readonly usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
- @Inject(DI.noteEditRepository)
- private noteEditRepository: NoteEditRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
@Inject(DI.accessTokensRepository)
- private accessTokensRepository: AccessTokensRepository,
- @Inject(DI.config)
- private config: Config,
- private userEntityService: UserEntityService,
- private driveService: DriveService,
- private mastoConverter: MastoConverters,
+ private readonly accessTokensRepository: AccessTokensRepository,
+ @Inject(DI.config)
+ private readonly config: Config,
+ private readonly driveService: DriveService,
+ private readonly mastoConverters: MastoConverters,
+ private readonly logger: MastodonLogger,
+ private readonly authenticateService: AuthenticateService,
) { }
@bindThis
+ public async getAuthClient(request: FastifyRequest): Promise<{ client: MegalodonInterface, me: MiLocalUser | null }> {
+ const accessToken = getAccessToken(request.headers.authorization);
+ const [me] = await this.authenticateService.authenticate(accessToken);
+
+ const baseUrl = `${request.protocol}://${request.host}`;
+ const client = megalodon('misskey', baseUrl, accessToken);
+
+ return { client, me };
+ }
+
+ @bindThis
+ public async getAuthOnly(request: FastifyRequest): Promise<MiLocalUser | null> {
+ const accessToken = getAccessToken(request.headers.authorization);
+ const [me] = await this.authenticateService.authenticate(accessToken);
+ return me;
+ }
+
+ @bindThis
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
const upload = multer({
storage: multer.diskStorage({}),
@@ -59,12 +84,12 @@ export class MastodonApiServerService {
},
});
- fastify.addHook('onRequest', (request, reply, done) => {
+ fastify.addHook('onRequest', (_, reply, done) => {
reply.header('Access-Control-Allow-Origin', '*');
done();
});
- fastify.addContentTypeParser('application/x-www-form-urlencoded', (request, payload, done) => {
+ fastify.addContentTypeParser('application/x-www-form-urlencoded', (_, payload, done) => {
let body = '';
payload.on('data', (data) => {
body += data;
@@ -73,8 +98,8 @@ export class MastodonApiServerService {
try {
const parsed = querystring.parse(body);
done(null, parsed);
- } catch (e: any) {
- done(e);
+ } catch (e) {
+ done(e as Error);
}
});
payload.on('error', done);
@@ -83,20 +108,21 @@ export class MastodonApiServerService {
fastify.register(multer.contentParser);
fastify.get('/v1/custom_emojis', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getInstanceCustomEmojis();
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/custom_emojis', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/instance', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
@@ -111,118 +137,124 @@ export class MastodonApiServerService {
},
order: { id: 'ASC' },
});
- const contact = admin == null ? null : await this.mastoConverter.convertAccount((await client.getAccount(admin.id)).data);
+ const contact = admin == null ? null : await this.mastoConverters.convertAccount((await client.getAccount(admin.id)).data);
reply.send(await getInstance(data.data, contact as Entity.Account, this.config, this.serverSettings));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/instance', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/announcements', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getInstanceAnnouncements();
reply.send(data.data.map((announcement) => convertAnnouncement(announcement)));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/announcements', data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Body: { id: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<{ Body: { id?: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const data = await client.dismissInstanceAnnouncement(
- _request.body['id'],
- );
+ if (!_request.body.id) return reply.code(400).send({ error: 'Missing required payload "id"' });
+ const data = await client.dismissInstanceAnnouncement(_request.body['id']);
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/announcements/${_request.body.id}/dismiss`, data);
+ reply.code(401).send(data);
}
- },
- );
+ });
fastify.post('/v1/media', { preHandler: upload.single('file') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const multipartData = await _request.file;
+ const multipartData = await _request.file();
if (!multipartData) {
reply.code(401).send({ error: 'No image' });
return;
}
const data = await client.uploadMedia(multipartData);
reply.send(convertAttachment(data.data as Entity.Attachment));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/media', data);
+ reply.code(401).send(data);
}
});
- fastify.post('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<{ Body: { description?: string; focus?: string }}>('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const multipartData = await _request.file;
+ const multipartData = await _request.file();
if (!multipartData) {
reply.code(401).send({ error: 'No image' });
return;
}
- const data = await client.uploadMedia(multipartData, _request.body!);
+ const data = await client.uploadMedia(multipartData, _request.body);
reply.send(convertAttachment(data.data as Entity.Attachment));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v2/media', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/filters', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getFilters();
reply.send(data.data.map((filter) => convertFilter(filter)));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/filters', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/trends', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getInstanceTrends();
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/trends', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/trends/tags', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getInstanceTrends();
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/trends/tags', data);
+ reply.code(401).send(data);
}
});
@@ -231,50 +263,69 @@ export class MastodonApiServerService {
reply.send([]);
});
- fastify.post('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<AuthMastodonRoute>('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const client = getClient(BASE_URL, ''); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await ApiAuthMastodon(_request, client);
reply.send(data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/apps', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/preferences', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getPreferences();
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/preferences', data);
+ reply.code(401).send(data);
}
});
//#region Accounts
- fastify.get('/v1/accounts/verify_credentials', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
- // displayed without being logged in
+ fastify.get<ApiAccountMastodonRoute>('/v1/accounts/verify_credentials', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.verifyCredentials());
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/accounts/verify_credentials', data);
+ reply.code(401).send(data);
}
});
- fastify.patch('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.patch<{
+ Body: {
+ discoverable?: string,
+ bot?: string,
+ display_name?: string,
+ note?: string,
+ avatar?: string,
+ header?: string,
+ locked?: string,
+ source?: {
+ privacy?: string,
+ sensitive?: string,
+ language?: string,
+ },
+ fields_attributes?: {
+ name: string,
+ value: string,
+ }[],
+ },
+ }>('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
@@ -332,512 +383,495 @@ export class MastodonApiServerService {
(_request.body as any).fields_attributes = fields.filter((field: any) => field.name.trim().length > 0 && field.value.length > 0);
}
- const data = await client.updateCredentials(_request.body!);
- reply.send(await this.mastoConverter.convertAccount(data.data));
- } catch (e: any) {
- //console.error(e);
- reply.code(401).send(e.response.data);
+ const options = {
+ ..._request.body,
+ discoverable: toBoolean(_request.body.discoverable),
+ bot: toBoolean(_request.body.bot),
+ locked: toBoolean(_request.body.locked),
+ source: _request.body.source ? {
+ ..._request.body.source,
+ sensitive: toBoolean(_request.body.source.sensitive),
+ } : undefined,
+ };
+ const data = await client.updateCredentials(options);
+ reply.send(await this.mastoConverters.convertAccount(data.data));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('PATCH /v1/accounts/update_credentials', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/accounts/lookup', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Querystring: { acct?: string }}>('/v1/accounts/lookup', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
- // displayed without being logged in
+ const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isn't displayed without being logged in
try {
- const data = await client.search((_request.query as any).acct, { type: 'accounts' });
+ if (!_request.query.acct) return reply.code(400).send({ error: 'Missing required property "acct"' });
+ const data = await client.search(_request.query.acct, { type: 'accounts' });
const profile = await this.userProfilesRepository.findOneBy({ userId: data.data.accounts[0].id });
- data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) || [];
- reply.send(await this.mastoConverter.convertAccount(data.data.accounts[0]));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) ?? [];
+ reply.send(await this.mastoConverters.convertAccount(data.data.accounts[0]));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/accounts/lookup', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/accounts/relationships', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
- // displayed without being logged in
- let users;
+ fastify.get<ApiAccountMastodonRoute & { Querystring: { id?: string | string[], 'id[]'?: string | string[] }}>('/v1/accounts/relationships', async (_request, reply) => {
try {
- let ids = _request.query ? (_request.query as any)['id[]'] ?? (_request.query as any)['id'] : null;
+ const { client, me } = await this.getAuthClient(_request);
+ let ids = _request.query['id[]'] ?? _request.query['id'] ?? [];
if (typeof ids === 'string') {
ids = [ids];
}
- users = ids;
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
- reply.send(await account.getRelationships(users));
- } catch (e: any) {
- /* console.error(e); */
- const data = e.response.data;
- data.users = users;
- console.error(data);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
+ reply.send(await account.getRelationships(ids));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/accounts/relationships', data);
reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const sharkId = _request.params.id;
- const data = await client.getAccount(sharkId);
- const account = await this.mastoConverter.convertAccount(data.data);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const data = await client.getAccount(_request.params.id);
+ const account = await this.mastoConverters.convertAccount(data.data);
reply.send(account);
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/statuses', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/statuses', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getStatuses());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/statuses`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getFeaturedTags();
reply.send(data.data.map((tag) => convertFeaturedTag(tag)));
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/featured_tags`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/followers', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/followers', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getFollowers());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/followers`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/following', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/following', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getFollowing());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/following`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getAccountLists(_request.params.id);
reply.send(data.data.map((list) => convertList(list)));
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/lists`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.addFollow());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/follow`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.rmFollow());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/unfollow`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.addBlock());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/block`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.rmBlock());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/unblock`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.addMute());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/mute`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.rmMute());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/unmute`, data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/followed_tags', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getFollowedTags();
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/followed_tags', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/bookmarks', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute>('/v1/bookmarks', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getBookmarks());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/bookmarks', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/favourites', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute>('/v1/favourites', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getFavourites());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/favourites', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/mutes', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute>('/v1/mutes', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getMutes());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/mutes', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/blocks', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute>('/v1/blocks', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getBlocks());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/blocks', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/follow_requests', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Querystring: { limit?: string }}>('/v1/follow_requests', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const data = await client.getFollowRequests( ((_request.query as any) || { limit: 20 }).limit );
- reply.send(await Promise.all(data.data.map(async (account) => await this.mastoConverter.convertAccount(account as Entity.Account))));
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ const limit = _request.query.limit ? parseInt(_request.query.limit) : 20;
+ const data = await client.getFollowRequests(limit);
+ reply.send(await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account as Entity.Account))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/follow_requests', data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.acceptFollow());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/follow_requests/${_request.params.id}/authorize`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.rejectFollow());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/follow_requests/${_request.params.id}/reject`, data);
+ reply.code(401).send(data);
}
});
//#endregion
//#region Search
- fastify.get('/v1/search', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiSearchMastodonRoute>('/v1/search', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
try {
- const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
reply.send(await search.SearchV1());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/search', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v2/search', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiSearchMastodonRoute>('/v2/search', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
try {
- const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
reply.send(await search.SearchV2());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v2/search', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/trends/statuses', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiSearchMastodonRoute>('/v1/trends/statuses', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
try {
- const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
reply.send(await search.getStatusTrends());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/trends/statuses', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v2/suggestions', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiSearchMastodonRoute>('/v2/suggestions', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
try {
- const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
reply.send(await search.getSuggestions());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v2/suggestions', data);
+ reply.code(401).send(data);
}
});
//#endregion
//#region Notifications
- fastify.get('/v1/notifications', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiNotifyMastodonRoute>('/v1/notifications', async (_request, reply) => {
try {
- const notify = new ApiNotifyMastodon(_request, client);
+ const { client, me } = await this.getAuthClient(_request);
+ const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
reply.send(await notify.getNotifications());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/notifications', data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/notification/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id', async (_request, reply) => {
try {
- const notify = new ApiNotifyMastodon(_request, client);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
reply.send(await notify.getNotification());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/notification/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const notify = new ApiNotifyMastodon(_request, client);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
reply.send(await notify.rmNotification());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/notification/${_request.params.id}/dismiss`, data);
+ reply.code(401).send(data);
}
});
- fastify.post('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiNotifyMastodonRoute>('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const notify = new ApiNotifyMastodon(_request, client);
+ const { client, me } = await this.getAuthClient(_request);
+ const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
reply.send(await notify.rmNotifications());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/notifications/clear', data);
+ reply.code(401).send(data);
}
});
//#endregion
//#region Filters
- fastify.get<{ Params: { id: string } }>('/v1/filters/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const filter = new ApiFilterMastodon(_request, client);
- !_request.params.id ? reply.send(await filter.getFilters()) : reply.send(await filter.getFilter());
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ _request.params.id
+ ? reply.send(await filter.getFilter())
+ : reply.send(await filter.getFilters());
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/filters/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
- fastify.post('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<ApiFilterMastodonRoute>('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.createFilter());
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/filters', data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.updateFilter());
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/filters/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
- fastify.delete<{ Params: { id: string } }>('/v1/filters/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.delete<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.rmFilter());
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`DELETE /v1/filters/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
//#endregion
//#region Timelines
- const TLEndpoint = new ApiTimelineMastodon(fastify, this.config, this.mastoConverter);
+ const TLEndpoint = new ApiTimelineMastodon(fastify, this.mastoConverters, this.logger, this);
// GET Endpoints
TLEndpoint.getTL();
@@ -862,7 +896,7 @@ export class MastodonApiServerService {
//#endregion
//#region Status
- const NoteEndpoint = new ApiStatusMastodon(fastify, this.mastoConverter);
+ const NoteEndpoint = new ApiStatusMastodon(fastify, this.mastoConverters, this.logger, this.authenticateService, this);
// GET Endpoints
NoteEndpoint.getStatus();
@@ -889,16 +923,32 @@ export class MastodonApiServerService {
NoteEndpoint.votePoll();
// PUT Endpoint
- fastify.put<{ Params: { id: string } }>('/v1/media/:id', { preHandler: upload.none() }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.put<{
+ Params: {
+ id?: string,
+ },
+ Body: {
+ file?: unknown,
+ description?: string,
+ focus?: string,
+ is_sensitive?: string,
+ },
+ }>('/v1/media/:id', { preHandler: upload.none() }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const data = await client.updateMedia(_request.params.id, _request.body!);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const options = {
+ ..._request.body,
+ is_sensitive: toBoolean(_request.body.is_sensitive),
+ };
+ const data = await client.updateMedia(_request.params.id, options);
reply.send(convertAttachment(data.data));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`PUT /v1/media/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
NoteEndpoint.updateStatus();
diff --git a/packages/backend/src/server/api/mastodon/MastodonDataService.ts b/packages/backend/src/server/api/mastodon/MastodonDataService.ts
new file mode 100644
index 0000000000..671ecdcbed
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/MastodonDataService.ts
@@ -0,0 +1,84 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { IsNull } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { QueryService } from '@/core/QueryService.js';
+import type { MiNote, NotesRepository } from '@/models/_.js';
+import type { MiLocalUser } from '@/models/User.js';
+import { ApiError } from '../error.js';
+
+/**
+ * Utility service for accessing data with Mastodon semantics
+ */
+@Injectable()
+export class MastodonDataService {
+ constructor(
+ @Inject(DI.notesRepository)
+ private readonly notesRepository: NotesRepository,
+
+ @Inject(QueryService)
+ private readonly queryService: QueryService,
+ ) {}
+
+ /**
+ * Fetches a note in the context of the current user, and throws an exception if not found.
+ */
+ public async requireNote(noteId: string, me?: MiLocalUser | null): Promise<MiNote> {
+ const note = await this.getNote(noteId, me);
+
+ if (!note) {
+ throw new ApiError({
+ message: 'No such note.',
+ code: 'NO_SUCH_NOTE',
+ id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
+ kind: 'client',
+ httpStatusCode: 404,
+ });
+ }
+
+ return note;
+ }
+
+ /**
+ * Fetches a note in the context of the current user.
+ */
+ public async getNote(noteId: string, me?: MiLocalUser | null): Promise<MiNote | null> {
+ // Root query: note + required dependencies
+ const query = this.notesRepository
+ .createQueryBuilder('note')
+ .where('note.id = :noteId', { noteId })
+ .innerJoinAndSelect('note.user', 'user');
+
+ // Restrict visibility
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) {
+ this.queryService.generateBlockedUserQuery(query, me);
+ }
+
+ return await query.getOne();
+ }
+
+ /**
+ * Checks where the current user has made a reblog / boost / pure renote of a given target note.
+ */
+ public async hasReblog(noteId: string, me: MiLocalUser | null | undefined): Promise<boolean> {
+ if (!me) return false;
+
+ return await this.notesRepository.existsBy({
+ // Reblog of the target note by me
+ userId: me.id,
+ renoteId: noteId,
+
+ // That is pure (not a quote)
+ text: IsNull(),
+ cw: IsNull(),
+ replyId: IsNull(),
+ hasPoll: false,
+ fileIds: '{}',
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/mastodon/MastodonLogger.ts b/packages/backend/src/server/api/mastodon/MastodonLogger.ts
new file mode 100644
index 0000000000..bb844773c4
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/MastodonLogger.ts
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import Logger, { Data } from '@/logger.js';
+import { LoggerService } from '@/core/LoggerService.js';
+
+@Injectable()
+export class MastodonLogger {
+ public readonly logger: Logger;
+
+ constructor(loggerService: LoggerService) {
+ this.logger = loggerService.getLogger('masto-api');
+ }
+
+ public error(endpoint: string, error: Data): void {
+ this.logger.error(`Error in mastodon API endpoint ${endpoint}:`, error);
+ }
+}
+
+export function getErrorData(error: unknown): Data {
+ if (error == null) return {};
+ if (typeof(error) === 'string') return error;
+ if (typeof(error) === 'object') {
+ if ('response' in error) {
+ if (typeof(error.response) === 'object' && error.response) {
+ if ('data' in error.response) {
+ if (typeof(error.response.data) === 'object' && error.response.data) {
+ return error.response.data as Record<string, unknown>;
+ }
+ }
+ }
+ }
+ return error as Record<string, unknown>;
+ }
+ return { error };
+}
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index 405701e826..b6ff5bc59a 100644
--- a/packages/backend/src/server/api/mastodon/converters.ts
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -9,18 +9,32 @@ import mfm from '@transfem-org/sfm-js';
import { DI } from '@/di-symbols.js';
import { MfmService } from '@/core/MfmService.js';
import type { Config } from '@/config.js';
-import type { IMentionedRemoteUsers } from '@/models/Note.js';
-import type { MiUser } from '@/models/User.js';
-import type { NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IMentionedRemoteUsers, MiNote } from '@/models/Note.js';
+import type { MiLocalUser, MiUser } from '@/models/User.js';
+import type { NoteEditRepository, UserProfilesRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { IdService } from '@/core/IdService.js';
-import { GetterService } from '../GetterService.js';
+import type { Packed } from '@/misc/json-schema.js';
+import { MastodonDataService } from '@/server/api/mastodon/MastodonDataService.js';
+import { GetterService } from '@/server/api/GetterService.js';
-export enum IdConvertType {
- MastodonId,
- SharkeyId,
+// Missing from Megalodon apparently
+// https://docs.joinmastodon.org/entities/StatusEdit/
+export interface StatusEdit {
+ content: string;
+ spoiler_text: string;
+ sensitive: boolean;
+ created_at: string;
+ account: MastodonEntity.Account;
+ poll?: {
+ options: {
+ title: string;
+ }[]
+ },
+ media_attachments: MastodonEntity.Attachment[],
+ emojis: MastodonEntity.Emoji[],
}
export const escapeMFM = (text: string): string => text
@@ -36,27 +50,25 @@ export const escapeMFM = (text: string): string => text
export class MastoConverters {
constructor(
@Inject(DI.config)
- private config: Config,
-
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
+ private readonly config: Config,
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
@Inject(DI.noteEditRepository)
- private noteEditRepository: NoteEditRepository,
+ private readonly noteEditRepository: NoteEditRepository,
- private mfmService: MfmService,
- private getterService: GetterService,
- private customEmojiService: CustomEmojiService,
- private idService: IdService,
- private driveFileEntityService: DriveFileEntityService,
- ) {
- }
+ private readonly mfmService: MfmService,
+ private readonly getterService: GetterService,
+ private readonly customEmojiService: CustomEmojiService,
+ private readonly idService: IdService,
+ private readonly driveFileEntityService: DriveFileEntityService,
+ private readonly mastodonDataService: MastodonDataService,
+ ) {}
- private encode(u: MiUser, m: IMentionedRemoteUsers): Entity.Mention {
+ private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention {
let acct = u.username;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let acctUrl = `https://${u.host || this.config.host}/@${u.username}`;
let url: string | null = null;
if (u.host) {
@@ -89,7 +101,11 @@ export class MastoConverters {
return 'unknown';
}
- public encodeFile(f: any): Entity.Attachment {
+ public encodeFile(f: Packed<'DriveFile'>): MastodonEntity.Attachment {
+ const { width, height } = f.properties;
+ const size = (width && height) ? `${width}x${height}` : undefined;
+ const aspect = (width && height) ? (width / height) : undefined;
+
return {
id: f.id,
type: this.fileType(f.type),
@@ -98,11 +114,19 @@ export class MastoConverters {
preview_url: f.thumbnailUrl,
text_url: f.url,
meta: {
- width: f.properties.width,
- height: f.properties.height,
+ original: {
+ width,
+ height,
+ size,
+ aspect,
+ },
+ width,
+ height,
+ size,
+ aspect,
},
- description: f.comment ? f.comment : null,
- blurhash: f.blurhash ? f.blurhash : null,
+ description: f.comment ?? null,
+ blurhash: f.blurhash ?? null,
};
}
@@ -112,7 +136,7 @@ export class MastoConverters {
});
}
- private async encodeField(f: Entity.Field): Promise<Entity.Field> {
+ private async encodeField(f: Entity.Field): Promise<MastodonEntity.Field> {
return {
name: f.name,
value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
@@ -120,7 +144,7 @@ export class MastoConverters {
};
}
- public async convertAccount(account: Entity.Account | MiUser) {
+ public async convertAccount(account: Entity.Account | MiUser): Promise<MastodonEntity.Account> {
const user = await this.getUser(account.id);
const profile = await this.userProfilesRepository.findOneBy({ userId: user.id });
const emojis = await this.customEmojiService.populateEmojis(user.emojis, user.host ? user.host : this.config.host);
@@ -137,6 +161,7 @@ export class MastoConverters {
});
const fqn = `${user.username}@${user.host ?? this.config.hostname}`;
let acct = user.username;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let acctUrl = `https://${user.host || this.config.host}/@${user.username}`;
const acctUri = `https://${this.config.host}/users/${user.id}`;
if (user.host) {
@@ -166,19 +191,23 @@ export class MastoConverters {
fields: Promise.all(profile?.fields.map(async p => this.encodeField(p)) ?? []),
bot: user.isBot,
discoverable: user.isExplorable,
+ noindex: user.noindex,
+ group: null,
+ suspended: user.isSuspended,
+ limited: user.isSilenced,
});
}
- public async getEdits(id: string) {
- const note = await this.getterService.getNote(id);
+ public async getEdits(id: string, me?: MiLocalUser | null) {
+ const note = await this.mastodonDataService.getNote(id, me);
if (!note) {
- return {};
+ return [];
}
-
const noteUser = await this.getUser(note.userId).then(async (p) => await this.convertAccount(p));
const edits = await this.noteEditRepository.find({ where: { noteId: note.id }, order: { id: 'ASC' } });
- const history: Promise<any>[] = [];
+ const history: Promise<StatusEdit>[] = [];
+ // TODO this looks wrong, according to mastodon docs
let lastDate = this.idService.parse(note.id).date;
for (const edit of edits) {
const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
@@ -187,9 +216,8 @@ export class MastoConverters {
content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
created_at: lastDate.toISOString(),
emojis: [],
- sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
+ sensitive: edit.cw != null && edit.cw.length > 0,
spoiler_text: edit.cw ?? '',
- poll: null,
media_attachments: files.then(files => files.length > 0 ? files.map((f) => this.encodeFile(f)) : []),
};
lastDate = edit.updatedAt;
@@ -199,15 +227,16 @@ export class MastoConverters {
return await Promise.all(history);
}
- private async convertReblog(status: Entity.Status | null): Promise<any> {
+ private async convertReblog(status: Entity.Status | null, me?: MiLocalUser | null): Promise<MastodonEntity.Status | null> {
if (!status) return null;
- return await this.convertStatus(status);
+ return await this.convertStatus(status, me);
}
- public async convertStatus(status: Entity.Status) {
+ public async convertStatus(status: Entity.Status, me?: MiLocalUser | null): Promise<MastodonEntity.Status> {
const convertedAccount = this.convertAccount(status.account);
- const note = await this.getterService.getNote(status.id);
+ const note = await this.mastodonDataService.requireNote(status.id, me);
const noteUser = await this.getUser(status.account.id);
+ const mentionedRemoteUsers = JSON.parse(note.mentionedRemoteUsers);
const emojis = await this.customEmojiService.populateEmojis(note.emojis, noteUser.host ? noteUser.host : this.config.host);
const emoji: Entity.Emoji[] = [];
@@ -224,7 +253,7 @@ export class MastoConverters {
const mentions = Promise.all(note.mentions.map(p =>
this.getUser(p)
- .then(u => this.encode(u, JSON.parse(note.mentionedRemoteUsers)))
+ .then(u => this.encode(u, mentionedRemoteUsers))
.catch(() => null)))
.then(p => p.filter(m => m)) as Promise<Entity.Mention[]>;
@@ -235,20 +264,26 @@ export class MastoConverters {
} as Entity.Tag;
});
- const isQuote = note.renoteId && note.text ? true : false;
+ // This must mirror the usual isQuote / isPureRenote logic used elsewhere.
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const isQuote = note.renoteId && (note.text || note.cw || note.fileIds.length > 0 || note.hasPoll || note.replyId);
- const renote = note.renoteId ? this.getterService.getNote(note.renoteId) : null;
+ const renote: Promise<MiNote> | null = note.renoteId ? this.mastodonDataService.requireNote(note.renoteId, me) : null;
const quoteUri = Promise.resolve(renote).then(renote => {
if (!renote || !isQuote) return null;
return renote.url ?? renote.uri ?? `${this.config.url}/notes/${renote.id}`;
});
- const content = note.text !== null
- ? quoteUri.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
- .then(p => p ?? escapeMFM(note.text!))
+ const text = note.text;
+ const content = text !== null
+ ? quoteUri
+ .then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(text), mentionedRemoteUsers, false, quoteUri))
+ .then(p => p ?? escapeMFM(text))
: '';
+ const reblogged = await this.mastodonDataService.hasReblog(note.id, me);
+
// noinspection ES6MissingAwait
return await awaitAll({
id: note.id,
@@ -257,7 +292,7 @@ export class MastoConverters {
account: convertedAccount,
in_reply_to_id: note.replyId,
in_reply_to_account_id: note.replyUserId,
- reblog: !isQuote ? await this.convertReblog(status.reblog) : null,
+ reblog: !isQuote ? await this.convertReblog(status.reblog, me) : null,
content: content,
content_type: 'text/x.misskeymarkdown',
text: note.text,
@@ -266,34 +301,51 @@ export class MastoConverters {
replies_count: note.repliesCount,
reblogs_count: note.renoteCount,
favourites_count: status.favourites_count,
- reblogged: false,
+ reblogged,
favourited: status.favourited,
muted: status.muted,
sensitive: status.sensitive,
- spoiler_text: note.cw ? note.cw : '',
+ spoiler_text: note.cw ?? '',
visibility: status.visibility,
- media_attachments: status.media_attachments,
+ media_attachments: status.media_attachments.map(a => convertAttachment(a)),
mentions: mentions,
tags: tags,
card: null, //FIXME
poll: status.poll ?? null,
application: null, //FIXME
language: null, //FIXME
- pinned: false,
+ pinned: false, //FIXME
reactions: status.emoji_reactions,
emoji_reactions: status.emoji_reactions,
- bookmarked: false,
- quote: isQuote ? await this.convertReblog(status.reblog) : null,
- // optional chaining cannot be used, as it evaluates to undefined, not null
- edited_at: note.updatedAt ? note.updatedAt.toISOString() : null,
+ bookmarked: false, //FIXME
+ quote: isQuote ? await this.convertReblog(status.reblog, me) : null,
+ edited_at: note.updatedAt?.toISOString() ?? null,
});
}
+
+ public async convertConversation(conversation: Entity.Conversation, me?: MiLocalUser | null): Promise<MastodonEntity.Conversation> {
+ return {
+ id: conversation.id,
+ accounts: await Promise.all(conversation.accounts.map(a => this.convertAccount(a))),
+ last_status: conversation.last_status ? await this.convertStatus(conversation.last_status, me) : null,
+ unread: conversation.unread,
+ };
+ }
+
+ public async convertNotification(notification: Entity.Notification, me?: MiLocalUser | null): Promise<MastodonEntity.Notification> {
+ return {
+ account: await this.convertAccount(notification.account),
+ created_at: notification.created_at,
+ id: notification.id,
+ status: notification.status ? await this.convertStatus(notification.status, me) : undefined,
+ type: notification.type,
+ };
+ }
}
-function simpleConvert(data: any) {
+function simpleConvert<T>(data: T): T {
// copy the object to bypass weird pass by reference bugs
- const result = Object.assign({}, data);
- return result;
+ return Object.assign({}, data);
}
export function convertAccount(account: Entity.Account) {
@@ -302,8 +354,30 @@ export function convertAccount(account: Entity.Account) {
export function convertAnnouncement(announcement: Entity.Announcement) {
return simpleConvert(announcement);
}
-export function convertAttachment(attachment: Entity.Attachment) {
- return simpleConvert(attachment);
+export function convertAttachment(attachment: Entity.Attachment): MastodonEntity.Attachment {
+ const { width, height } = attachment.meta?.original ?? attachment.meta ?? {};
+ const size = (width && height) ? `${width}x${height}` : undefined;
+ const aspect = (width && height) ? (width / height) : undefined;
+ return {
+ ...attachment,
+ meta: attachment.meta ? {
+ ...attachment.meta,
+ original: {
+ ...attachment.meta.original,
+ width,
+ height,
+ size,
+ aspect,
+ frame_rate: String(attachment.meta.fps),
+ duration: attachment.meta.duration,
+ bitrate: attachment.meta.audio_bitrate ? parseInt(attachment.meta.audio_bitrate) : undefined,
+ },
+ width,
+ height,
+ size,
+ aspect,
+ } : null,
+ };
}
export function convertFilter(filter: Entity.Filter) {
return simpleConvert(filter);
@@ -315,45 +389,40 @@ export function convertFeaturedTag(tag: Entity.FeaturedTag) {
return simpleConvert(tag);
}
-export function convertNotification(notification: Entity.Notification) {
- notification.account = convertAccount(notification.account);
- if (notification.status) notification.status = convertStatus(notification.status);
- return notification;
-}
-
export function convertPoll(poll: Entity.Poll) {
return simpleConvert(poll);
}
+
+// noinspection JSUnusedGlobalSymbols
export function convertReaction(reaction: Entity.Reaction) {
if (reaction.accounts) {
reaction.accounts = reaction.accounts.map(convertAccount);
}
return reaction;
}
-export function convertRelationship(relationship: Entity.Relationship) {
- return simpleConvert(relationship);
-}
-export function convertStatus(status: Entity.Status) {
- status.account = convertAccount(status.account);
- status.media_attachments = status.media_attachments.map((attachment) =>
- convertAttachment(attachment),
- );
- if (status.poll) status.poll = convertPoll(status.poll);
- if (status.reblog) status.reblog = convertStatus(status.reblog);
-
- return status;
+// Megalodon sometimes returns broken / stubbed relationship data
+export function convertRelationship(relationship: Partial<Entity.Relationship> & { id: string }): MastodonEntity.Relationship {
+ return {
+ id: relationship.id,
+ following: relationship.following ?? false,
+ showing_reblogs: relationship.showing_reblogs ?? true,
+ notifying: relationship.notifying ?? true,
+ languages: [],
+ followed_by: relationship.followed_by ?? false,
+ blocking: relationship.blocking ?? false,
+ blocked_by: relationship.blocked_by ?? false,
+ muting: relationship.muting ?? false,
+ muting_notifications: relationship.muting_notifications ?? false,
+ requested: relationship.requested ?? false,
+ requested_by: relationship.requested_by ?? false,
+ domain_blocking: relationship.domain_blocking ?? false,
+ endorsed: relationship.endorsed ?? false,
+ note: relationship.note ?? '',
+ };
}
+// noinspection JSUnusedGlobalSymbols
export function convertStatusSource(status: Entity.StatusSource) {
return simpleConvert(status);
}
-
-export function convertConversation(conversation: Entity.Conversation) {
- conversation.accounts = conversation.accounts.map(convertAccount);
- if (conversation.last_status) {
- conversation.last_status = convertStatus(conversation.last_status);
- }
-
- return conversation;
-}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 6fcfb0019c..79cdddcb9e 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -3,273 +3,149 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { Injectable } from '@nestjs/common';
+import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
+import { MiLocalUser } from '@/models/User.js';
import { MastoConverters, convertRelationship } from '../converters.js';
-import { argsToBools, limitToInt } from './timeline.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
-import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import type { Config } from '@/config.js';
-import { Injectable } from '@nestjs/common';
-const relationshipModel = {
- id: '',
- following: false,
- followed_by: false,
- delivery_following: false,
- blocking: false,
- blocked_by: false,
- muting: false,
- muting_notifications: false,
- requested: false,
- domain_blocking: false,
- showing_reblogs: false,
- endorsed: false,
- notifying: false,
- note: '',
-};
+export interface ApiAccountMastodonRoute {
+ Params: { id?: string },
+ Querystring: TimelineArgs & { acct?: string },
+ Body: { notifications?: boolean }
+}
@Injectable()
export class ApiAccountMastodon {
- private request: FastifyRequest;
- private client: MegalodonInterface;
- private BASE_URL: string;
-
- constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoconverter: MastoConverters) {
- this.request = request;
- this.client = client;
- this.BASE_URL = BASE_URL;
- }
+ constructor(
+ private readonly request: FastifyRequest<ApiAccountMastodonRoute>,
+ private readonly client: MegalodonInterface,
+ private readonly me: MiLocalUser | null,
+ private readonly mastoConverters: MastoConverters,
+ ) {}
public async verifyCredentials() {
- try {
- const data = await this.client.verifyAccountCredentials();
- const acct = await this.mastoconverter.convertAccount(data.data);
- const newAcct = Object.assign({}, acct, {
- source: {
- note: acct.note,
- fields: acct.fields,
- privacy: '',
- sensitive: false,
- language: '',
- },
- });
- return newAcct;
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- return e.response;
- }
+ const data = await this.client.verifyAccountCredentials();
+ const acct = await this.mastoConverters.convertAccount(data.data);
+ return Object.assign({}, acct, {
+ source: {
+ note: acct.note,
+ fields: acct.fields,
+ privacy: '',
+ sensitive: false,
+ language: '',
+ },
+ });
}
public async lookup() {
- try {
- const data = await this.client.search((this.request.query as any).acct, { type: 'accounts' });
- return this.mastoconverter.convertAccount(data.data.accounts[0]);
- } catch (e: any) {
- /* console.error(e)
- console.error(e.response.data); */
- return e.response;
- }
+ if (!this.request.query.acct) throw new Error('Missing required property "acct"');
+ const data = await this.client.search(this.request.query.acct, { type: 'accounts' });
+ return this.mastoConverters.convertAccount(data.data.accounts[0]);
}
- public async getRelationships(users: [string]) {
- try {
- relationshipModel.id = users.toString() || '1';
-
- if (!(users.length > 0)) {
- return [relationshipModel];
- }
-
- const reqIds = [];
- for (let i = 0; i < users.length; i++) {
- reqIds.push(users[i]);
- }
-
- const data = await this.client.getRelationships(reqIds);
- return data.data.map((relationship) => convertRelationship(relationship));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ public async getRelationships(reqIds: string[]) {
+ const data = await this.client.getRelationships(reqIds);
+ return data.data.map(relationship => convertRelationship(relationship));
}
public async getStatuses() {
- try {
- const data = await this.client.getAccountStatuses((this.request.params as any).id, argsToBools(limitToInt(this.request.query as any)));
- return await Promise.all(data.data.map(async (status) => await this.mastoconverter.convertStatus(status)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getAccountStatuses(this.request.params.id, parseTimelineArgs(this.request.query));
+ return await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, this.me)));
}
public async getFollowers() {
- try {
- const data = await this.client.getAccountFollowers(
- (this.request.params as any).id,
- limitToInt(this.request.query as any),
- );
- return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getAccountFollowers(
+ this.request.params.id,
+ parseTimelineArgs(this.request.query),
+ );
+ return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
}
public async getFollowing() {
- try {
- const data = await this.client.getAccountFollowing(
- (this.request.params as any).id,
- limitToInt(this.request.query as any),
- );
- return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getAccountFollowing(
+ this.request.params.id,
+ parseTimelineArgs(this.request.query),
+ );
+ return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
}
public async addFollow() {
- try {
- const data = await this.client.followAccount( (this.request.params as any).id );
- const acct = convertRelationship(data.data);
- acct.following = true;
- return acct;
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.followAccount(this.request.params.id);
+ const acct = convertRelationship(data.data);
+ acct.following = true;
+ return acct;
}
public async rmFollow() {
- try {
- const data = await this.client.unfollowAccount( (this.request.params as any).id );
- const acct = convertRelationship(data.data);
- acct.following = false;
- return acct;
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.unfollowAccount(this.request.params.id);
+ const acct = convertRelationship(data.data);
+ acct.following = false;
+ return acct;
}
public async addBlock() {
- try {
- const data = await this.client.blockAccount( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.blockAccount(this.request.params.id);
+ return convertRelationship(data.data);
}
public async rmBlock() {
- try {
- const data = await this.client.unblockAccount( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.unblockAccount(this.request.params.id);
+ return convertRelationship(data.data);
}
public async addMute() {
- try {
- const data = await this.client.muteAccount(
- (this.request.params as any).id,
- this.request.body as any,
- );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.muteAccount(
+ this.request.params.id,
+ this.request.body.notifications ?? true,
+ );
+ return convertRelationship(data.data);
}
public async rmMute() {
- try {
- const data = await this.client.unmuteAccount( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.unmuteAccount(this.request.params.id);
+ return convertRelationship(data.data);
}
public async getBookmarks() {
- try {
- const data = await this.client.getBookmarks( limitToInt(this.request.query as any) );
- return data.data.map((status) => this.mastoconverter.convertStatus(status));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ const data = await this.client.getBookmarks(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
}
public async getFavourites() {
- try {
- const data = await this.client.getFavourites( limitToInt(this.request.query as any) );
- return data.data.map((status) => this.mastoconverter.convertStatus(status));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ const data = await this.client.getFavourites(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
}
public async getMutes() {
- try {
- const data = await this.client.getMutes( limitToInt(this.request.query as any) );
- return data.data.map((account) => this.mastoconverter.convertAccount(account));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ const data = await this.client.getMutes(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
}
public async getBlocks() {
- try {
- const data = await this.client.getBlocks( limitToInt(this.request.query as any) );
- return data.data.map((account) => this.mastoconverter.convertAccount(account));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ const data = await this.client.getBlocks(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
}
public async acceptFollow() {
- try {
- const data = await this.client.acceptFollowRequest( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.acceptFollowRequest(this.request.params.id);
+ return convertRelationship(data.data);
}
public async rejectFollow() {
- try {
- const data = await this.client.rejectFollowRequest( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.rejectFollowRequest(this.request.params.id);
+ return convertRelationship(data.data);
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts
index a447bdb1b7..b58cc902da 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/auth.ts
@@ -44,36 +44,54 @@ const writeScope = [
'write:gallery-likes',
];
-export async function ApiAuthMastodon(request: FastifyRequest, client: MegalodonInterface) {
- const body: any = request.body || request.query;
- try {
- let scope = body.scopes;
- if (typeof scope === 'string') scope = scope.split(' ') || scope.split('+');
- const pushScope = new Set<string>();
- for (const s of scope) {
- if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
- if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
- }
- const scopeArr = Array.from(pushScope);
+export interface AuthPayload {
+ scopes?: string | string[],
+ redirect_uris?: string,
+ client_name?: string,
+ website?: string,
+}
+
+// Not entirely right, but it gets TypeScript to work so *shrug*
+export type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
- const red = body.redirect_uris;
- const appData = await client.registerApp(body.client_name, {
- scopes: scopeArr,
- redirect_uris: red,
- website: body.website,
- });
- const returns = {
- id: Math.floor(Math.random() * 100).toString(),
- name: appData.name,
- website: body.website,
- redirect_uri: red,
- client_id: Buffer.from(appData.url || '').toString('base64'),
- client_secret: appData.clientSecret,
- };
+export async function ApiAuthMastodon(request: FastifyRequest<AuthMastodonRoute>, client: MegalodonInterface) {
+ const body = request.body ?? request.query;
+ if (!body.scopes) throw new Error('Missing required payload "scopes"');
+ if (!body.redirect_uris) throw new Error('Missing required payload "redirect_uris"');
+ if (!body.client_name) throw new Error('Missing required payload "client_name"');
- return returns;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
+ let scope = body.scopes;
+ if (typeof scope === 'string') {
+ scope = scope.split(/[ +]/g);
}
+
+ const pushScope = new Set<string>();
+ for (const s of scope) {
+ if (s.match(/^read/)) {
+ for (const r of readScope) {
+ pushScope.add(r);
+ }
+ }
+ if (s.match(/^write/)) {
+ for (const r of writeScope) {
+ pushScope.add(r);
+ }
+ }
+ }
+
+ const red = body.redirect_uris;
+ const appData = await client.registerApp(body.client_name, {
+ scopes: Array.from(pushScope),
+ redirect_uris: red,
+ website: body.website,
+ });
+
+ return {
+ id: Math.floor(Math.random() * 100).toString(),
+ name: appData.name,
+ website: body.website,
+ redirect_uri: red,
+ client_id: Buffer.from(appData.url || '').toString('base64'), // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
+ client_secret: appData.clientSecret,
+ };
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
index ce6809d230..382f0a8f1f 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
@@ -3,68 +3,73 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { toBoolean } from '@/server/api/mastodon/timelineArgs.js';
import { convertFilter } from '../converters.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
-export class ApiFilterMastodon {
- private request: FastifyRequest;
- private client: MegalodonInterface;
-
- constructor(request: FastifyRequest, client: MegalodonInterface) {
- this.request = request;
- this.client = client;
+export interface ApiFilterMastodonRoute {
+ Params: {
+ id?: string,
+ },
+ Body: {
+ phrase?: string,
+ context?: string[],
+ irreversible?: string,
+ whole_word?: string,
+ expires_in?: string,
}
+}
+
+export class ApiFilterMastodon {
+ constructor(
+ private readonly request: FastifyRequest<ApiFilterMastodonRoute>,
+ private readonly client: MegalodonInterface,
+ ) {}
public async getFilters() {
- try {
- const data = await this.client.getFilters();
- return data.data.map((filter) => convertFilter(filter));
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ const data = await this.client.getFilters();
+ return data.data.map((filter) => convertFilter(filter));
}
public async getFilter() {
- try {
- const data = await this.client.getFilter( (this.request.params as any).id );
- return convertFilter(data.data);
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getFilter(this.request.params.id);
+ return convertFilter(data.data);
}
public async createFilter() {
- try {
- const body: any = this.request.body;
- const data = await this.client.createFilter(body.pharse, body.context, body);
- return convertFilter(data.data);
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
+ if (!this.request.body.context) throw new Error('Missing required payload "context"');
+ const options = {
+ phrase: this.request.body.phrase,
+ context: this.request.body.context,
+ irreversible: toBoolean(this.request.body.irreversible),
+ whole_word: toBoolean(this.request.body.whole_word),
+ expires_in: this.request.body.expires_in,
+ };
+ const data = await this.client.createFilter(this.request.body.phrase, this.request.body.context, options);
+ return convertFilter(data.data);
}
public async updateFilter() {
- try {
- const body: any = this.request.body;
- const data = await this.client.updateFilter((this.request.params as any).id, body.pharse, body.context);
- return convertFilter(data.data);
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
+ if (!this.request.body.context) throw new Error('Missing required payload "context"');
+ const options = {
+ phrase: this.request.body.phrase,
+ context: this.request.body.context,
+ irreversible: toBoolean(this.request.body.irreversible),
+ whole_word: toBoolean(this.request.body.whole_word),
+ expires_in: this.request.body.expires_in,
+ };
+ const data = await this.client.updateFilter(this.request.params.id, this.request.body.phrase, this.request.body.context, options);
+ return convertFilter(data.data);
}
public async rmFilter() {
- try {
- const data = await this.client.deleteFilter( (this.request.params as any).id );
- return data.data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.deleteFilter(this.request.params.id);
+ return data.data;
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
index c9833b85d7..48a56138cf 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
@@ -8,6 +8,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import type { Config } from '@/config.js';
import type { MiMeta } from '@/models/Meta.js';
+/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
export async function getInstance(
response: Entity.Instance,
contact: Entity.Account,
@@ -17,11 +18,8 @@ export async function getInstance(
return {
uri: config.url,
title: meta.name || 'Sharkey',
- short_description:
- meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
- description:
- meta.description ||
- 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
+ short_description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
+ description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
email: response.email || '',
version: `3.0.0 (compatible; Sharkey ${config.version})`,
urls: response.urls,
diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
index 0eefb5894c..14eee8565a 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
@@ -3,73 +3,56 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { convertNotification } from '../converters.js';
-import type { MegalodonInterface, Entity } from 'megalodon';
+import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
+import { MiLocalUser } from '@/models/User.js';
+import { MastoConverters } from '@/server/api/mastodon/converters.js';
+import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
-function toLimitToInt(q: any) {
- if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10);
- return q;
+export interface ApiNotifyMastodonRoute {
+ Params: {
+ id?: string,
+ },
+ Querystring: TimelineArgs,
}
export class ApiNotifyMastodon {
- private request: FastifyRequest;
- private client: MegalodonInterface;
-
- constructor(request: FastifyRequest, client: MegalodonInterface) {
- this.request = request;
- this.client = client;
- }
+ constructor(
+ private readonly request: FastifyRequest<ApiNotifyMastodonRoute>,
+ private readonly client: MegalodonInterface,
+ private readonly me: MiLocalUser | null,
+ private readonly mastoConverters: MastoConverters,
+ ) {}
public async getNotifications() {
- try {
- const data = await this.client.getNotifications( toLimitToInt(this.request.query) );
- const notifs = data.data;
- const processed = notifs.map((n: Entity.Notification) => {
- const convertedn = convertNotification(n);
- if (convertedn.type !== 'follow' && convertedn.type !== 'follow_request') {
- if (convertedn.type === 'reaction') convertedn.type = 'favourite';
- return convertedn;
- } else {
- return convertedn;
- }
- });
- return processed;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ const data = await this.client.getNotifications(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map(async n => {
+ const converted = await this.mastoConverters.convertNotification(n, this.me);
+ if (converted.type === 'reaction') {
+ converted.type = 'favourite';
+ }
+ return converted;
+ }));
}
public async getNotification() {
- try {
- const data = await this.client.getNotification( (this.request.params as any).id );
- const notif = convertNotification(data.data);
- if (notif.type !== 'follow' && notif.type !== 'follow_request' && notif.type === 'reaction') notif.type = 'favourite';
- return notif;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getNotification(this.request.params.id);
+ const converted = await this.mastoConverters.convertNotification(data.data, this.me);
+ if (converted.type === 'reaction') {
+ converted.type = 'favourite';
}
+ return converted;
}
public async rmNotification() {
- try {
- const data = await this.client.dismissNotification( (this.request.params as any).id );
- return data.data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.dismissNotification(this.request.params.id);
+ return data.data;
}
public async rmNotifications() {
- try {
- const data = await this.client.dismissNotifications();
- return data.data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ const data = await this.client.dismissNotifications();
+ return data.data;
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts
index 946e796e2a..4850b4652f 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/search.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts
@@ -3,88 +3,92 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { MiLocalUser } from '@/models/User.js';
import { MastoConverters } from '../converters.js';
-import { limitToInt } from './timeline.js';
+import { parseTimelineArgs, TimelineArgs } from '../timelineArgs.js';
+import Account = Entity.Account;
+import Status = Entity.Status;
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
-export class ApiSearchMastodon {
- private request: FastifyRequest;
- private client: MegalodonInterface;
- private BASE_URL: string;
-
- constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoConverter: MastoConverters) {
- this.request = request;
- this.client = client;
- this.BASE_URL = BASE_URL;
+export interface ApiSearchMastodonRoute {
+ Querystring: TimelineArgs & {
+ type?: 'accounts' | 'hashtags' | 'statuses';
+ q?: string;
}
+}
+
+export class ApiSearchMastodon {
+ constructor(
+ private readonly request: FastifyRequest<ApiSearchMastodonRoute>,
+ private readonly client: MegalodonInterface,
+ private readonly me: MiLocalUser | null,
+ private readonly BASE_URL: string,
+ private readonly mastoConverters: MastoConverters,
+ ) {}
public async SearchV1() {
- try {
- const query: any = limitToInt(this.request.query as any);
- const type = query.type || '';
- const data = await this.client.search(query.q, { type: type, ...query });
- return data.data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.query.q) throw new Error('Missing required property "q"');
+ const query = parseTimelineArgs(this.request.query);
+ const data = await this.client.search(this.request.query.q, { type: this.request.query.type, ...query });
+ return data.data;
}
public async SearchV2() {
- try {
- const query: any = limitToInt(this.request.query as any);
- const type = query.type;
- const acct = !type || type === 'accounts' ? await this.client.search(query.q, { type: 'accounts', ...query }) : null;
- const stat = !type || type === 'statuses' ? await this.client.search(query.q, { type: 'statuses', ...query }) : null;
- const tags = !type || type === 'hashtags' ? await this.client.search(query.q, { type: 'hashtags', ...query }) : null;
- const data = {
- accounts: await Promise.all(acct?.data.accounts.map(async (account: any) => await this.mastoConverter.convertAccount(account)) ?? []),
- statuses: await Promise.all(stat?.data.statuses.map(async (status: any) => await this.mastoConverter.convertStatus(status)) ?? []),
- hashtags: tags?.data.hashtags ?? [],
- };
- return data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.query.q) throw new Error('Missing required property "q"');
+ const query = parseTimelineArgs(this.request.query);
+ const type = this.request.query.type;
+ const acct = !type || type === 'accounts' ? await this.client.search(this.request.query.q, { type: 'accounts', ...query }) : null;
+ const stat = !type || type === 'statuses' ? await this.client.search(this.request.query.q, { type: 'statuses', ...query }) : null;
+ const tags = !type || type === 'hashtags' ? await this.client.search(this.request.query.q, { type: 'hashtags', ...query }) : null;
+ return {
+ accounts: await Promise.all(acct?.data.accounts.map(async (account: Account) => await this.mastoConverters.convertAccount(account)) ?? []),
+ statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverters.convertStatus(status, this.me)) ?? []),
+ hashtags: tags?.data.hashtags ?? [],
+ };
}
public async getStatusTrends() {
- try {
- const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
- {
- method: 'POST',
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({}),
- })
- .then(res => res.json())
- .then(data => data.map((status: any) => this.mastoConverter.convertStatus(status)));
- return data;
- } catch (e: any) {
- console.error(e);
- return [];
- }
+ const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
+ {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ i: this.request.headers.authorization?.replace('Bearer ', ''),
+ }),
+ })
+ .then(res => res.json() as Promise<Status[]>)
+ .then(data => data.map(status => this.mastoConverters.convertStatus(status, this.me)));
+ return Promise.all(data);
}
public async getSuggestions() {
- try {
- const data = await fetch(`${this.BASE_URL}/api/users`,
- {
- method: 'POST',
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ i: this.request.headers.authorization?.replace('Bearer ', ''), limit: parseInt((this.request.query as any).limit) || 20, origin: 'local', sort: '+follower', state: 'alive' }),
- }).then((res) => res.json()).then(data => data.map(((entry: any) => { return { source: 'global', account: entry }; })));
- return Promise.all(data.map(async (suggestion: any) => { suggestion.account = await this.mastoConverter.convertAccount(suggestion.account); return suggestion; }));
- } catch (e: any) {
- console.error(e);
- return [];
- }
+ const data = await fetch(`${this.BASE_URL}/api/users`,
+ {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ i: this.request.headers.authorization?.replace('Bearer ', ''),
+ limit: parseTimelineArgs(this.request.query).limit ?? 20,
+ origin: 'local',
+ sort: '+follower',
+ state: 'alive',
+ }),
+ })
+ .then(res => res.json() as Promise<Account[]>)
+ .then(data => data.map((entry => ({
+ source: 'global',
+ account: entry,
+ }))));
+ return Promise.all(data.map(async suggestion => {
+ suggestion.account = await this.mastoConverters.convertAccount(suggestion.account);
+ return suggestion;
+ }));
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index ddc99639fa..4c49a6a293 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -3,181 +3,212 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import querystring from 'querystring';
+import querystring, { ParsedUrlQueryInput } from 'querystring';
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
-import { convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js';
-import { getClient } from '../MastodonApiServerService.js';
-import { limitToInt } from './timeline.js';
+import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
+import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/timelineArgs.js';
+import { AuthenticateService } from '@/server/api/AuthenticateService.js';
+import { convertAttachment, convertPoll, MastoConverters } from '../converters.js';
+import { getAccessToken, getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance } from 'fastify';
-import type { Config } from '@/config.js';
-import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-function normalizeQuery(data: any) {
- const str = querystring.stringify(data);
+function normalizeQuery(data: Record<string, unknown>) {
+ const str = querystring.stringify(data as ParsedUrlQueryInput);
return querystring.parse(str);
}
export class ApiStatusMastodon {
- private fastify: FastifyInstance;
- private mastoconverter: MastoConverters;
+ constructor(
+ private readonly fastify: FastifyInstance,
+ private readonly mastoConverters: MastoConverters,
+ private readonly logger: MastodonLogger,
+ private readonly authenticateService: AuthenticateService,
+ private readonly mastodon: MastodonApiServerService,
+ ) {}
- constructor(fastify: FastifyInstance, mastoconverter: MastoConverters) {
- this.fastify = fastify;
- this.mastoconverter = mastoconverter;
- }
-
- public async getStatus() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getStatus() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
try {
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(_request.is404 ? 404 : 401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}`, data);
+ reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
- public async getStatusSource() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getStatusSource() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusSource(_request.params.id);
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(_request.is404 ? 404 : 401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/source`, data);
+ reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
- public async getContext() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/context', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- const query: any = _request.query;
+ public getContext() {
+ this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => {
try {
- const data = await client.getStatusContext(_request.params.id, limitToInt(query));
- data.data.ancestors = await Promise.all(data.data.ancestors.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
- data.data.descendants = await Promise.all(data.data.descendants.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
- reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(_request.is404 ? 404 : 401).send(e.response.data);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const { data } = await client.getStatusContext(_request.params.id, parseTimelineArgs(_request.query));
+ const ancestors = await Promise.all(data.ancestors.map(async status => await this.mastoConverters.convertStatus(status, me)));
+ const descendants = await Promise.all(data.descendants.map(async status => await this.mastoConverters.convertStatus(status, me)));
+ reply.send({ ancestors, descendants });
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/context`, data);
+ reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
- public async getHistory() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
+ public getHistory() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
try {
- const edits = await this.mastoconverter.getEdits(_request.params.id);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const [user] = await this.authenticateService.authenticate(getAccessToken(_request.headers.authorization));
+ const edits = await this.mastoConverters.getEdits(_request.params.id, user);
reply.send(edits);
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/history`, data);
+ reply.code(401).send(data);
}
});
}
- public async getReblogged() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getReblogged() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusRebloggedBy(_request.params.id);
- reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/reblogged_by`, data);
+ reply.code(401).send(data);
}
});
}
- public async getFavourites() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getFavourites() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusFavouritedBy(_request.params.id);
- reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/favourited_by`, data);
+ reply.code(401).send(data);
}
});
}
- public async getMedia() {
- this.fastify.get<{ Params: { id: string } }>('/v1/media/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getMedia() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getMedia(_request.params.id);
reply.send(convertAttachment(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/media/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async getPoll() {
- this.fastify.get<{ Params: { id: string } }>('/v1/polls/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getPoll() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getPoll(_request.params.id);
reply.send(convertPoll(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/polls/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async votePoll() {
- this.fastify.post<{ Params: { id: string } }>('/v1/polls/:id/votes', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public votePoll() {
+ this.fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const body: any = _request.body;
try {
- const data = await client.votePoll(_request.params.id, body.choices);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.body.choices) return reply.code(400).send({ error: 'Missing required payload "choices"' });
+ const data = await client.votePoll(_request.params.id, _request.body.choices);
reply.send(convertPoll(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/polls/${_request.params.id}/votes`, data);
+ reply.code(401).send(data);
}
});
}
- public async postStatus() {
- this.fastify.post('/v1/statuses', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- let body: any = _request.body;
+ public postStatus() {
+ this.fastify.post<{
+ Body: {
+ media_ids?: string[],
+ poll?: {
+ options?: string[],
+ expires_in?: string,
+ multiple?: string,
+ hide_totals?: string,
+ },
+ in_reply_to_id?: string,
+ sensitive?: string,
+ spoiler_text?: string,
+ visibility?: 'public' | 'unlisted' | 'private' | 'direct',
+ scheduled_at?: string,
+ language?: string,
+ quote_id?: string,
+ status?: string,
+
+ // Broken clients
+ 'poll[options][]'?: string[],
+ 'media_ids[]'?: string[],
+ }
+ }>('/v1/statuses', async (_request, reply) => {
+ let body = _request.body;
try {
- if (
- (!body.poll && body['poll[options][]']) ||
- (!body.media_ids && body['media_ids[]'])
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])
) {
body = normalizeQuery(body);
}
- const text = body.status ? body.status : ' ';
+ const text = body.status ??= ' ';
const removed = text.replace(/@\S+/g, '').replace(/\s|/g, '');
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
@@ -189,226 +220,253 @@ export class ApiStatusMastodon {
reply.send(a.data);
}
if (body.in_reply_to_id && removed === '/unreact') {
- try {
- const id = body.in_reply_to_id;
- const post = await client.getStatus(id);
- const react = post.data.emoji_reactions.filter((e: any) => e.me)[0].name;
- const data = await client.deleteEmojiReaction(id, react);
- reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
- }
+ const id = body.in_reply_to_id;
+ const post = await client.getStatus(id);
+ const react = post.data.emoji_reactions.filter(e => e.me)[0].name;
+ const data = await client.deleteEmojiReaction(id, react);
+ reply.send(data.data);
}
if (!body.media_ids) body.media_ids = undefined;
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
- const { sensitive } = body;
- body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive;
-
- if (body.poll) {
- if (
- body.poll.expires_in != null &&
- typeof body.poll.expires_in === 'string'
- ) body.poll.expires_in = parseInt(body.poll.expires_in);
- if (
- body.poll.multiple != null &&
- typeof body.poll.multiple === 'string'
- ) body.poll.multiple = body.poll.multiple === 'true';
- if (
- body.poll.hide_totals != null &&
- typeof body.poll.hide_totals === 'string'
- ) body.poll.hide_totals = body.poll.hide_totals === 'true';
+ if (body.poll && !body.poll.options) {
+ return reply.code(400).send({ error: 'Missing required payload "poll.options"' });
+ }
+ if (body.poll && !body.poll.expires_in) {
+ return reply.code(400).send({ error: 'Missing required payload "poll.expires_in"' });
}
- const data = await client.postStatus(text, body);
- reply.send(await this.mastoconverter.convertStatus(data.data as Entity.Status));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ const options = {
+ ...body,
+ sensitive: toBoolean(body.sensitive),
+ poll: body.poll ? {
+ options: body.poll.options!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ expires_in: toInt(body.poll.expires_in)!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ multiple: toBoolean(body.poll.multiple),
+ hide_totals: toBoolean(body.poll.hide_totals),
+ } : undefined,
+ };
+
+ const data = await client.postStatus(text, options);
+ reply.send(await this.mastoConverters.convertStatus(data.data as Entity.Status, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/statuses', data);
+ reply.code(401).send(data);
}
});
}
- public async updateStatus() {
- this.fastify.put<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- const body: any = _request.body;
+ public updateStatus() {
+ this.fastify.put<{
+ Params: { id: string },
+ Body: {
+ status?: string,
+ spoiler_text?: string,
+ sensitive?: string,
+ media_ids?: string[],
+ poll?: {
+ options?: string[],
+ expires_in?: string,
+ multiple?: string,
+ hide_totals?: string,
+ },
+ }
+ }>('/v1/statuses/:id', async (_request, reply) => {
try {
- if (!body.media_ids) body.media_ids = undefined;
- if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
- const data = await client.editStatus(_request.params.id, body);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(_request.is404 ? 404 : 401).send(e.response.data);
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const body = _request.body;
+
+ if (!body.media_ids || !body.media_ids.length) {
+ body.media_ids = undefined;
+ }
+
+ const options = {
+ ...body,
+ sensitive: toBoolean(body.sensitive),
+ poll: body.poll ? {
+ options: body.poll.options,
+ expires_in: toInt(body.poll.expires_in),
+ multiple: toBoolean(body.poll.multiple),
+ hide_totals: toBoolean(body.poll.hide_totals),
+ } : undefined,
+ };
+
+ const data = await client.editStatus(_request.params.id, options);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async addFavourite() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public addFavourite() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
try {
- const data = (await client.createEmojiReaction(_request.params.id, '❤')) as any;
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.createEmojiReaction(_request.params.id, '❤');
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/favorite`, data);
+ reply.code(401).send(data);
}
});
}
- public async rmFavourite() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public rmFavourite() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
try {
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/unfavorite`, data);
+ reply.code(401).send(data);
}
});
}
- public async reblogStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public reblogStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.reblogStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/reblog`, data);
+ reply.code(401).send(data);
}
});
}
- public async unreblogStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public unreblogStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unreblogStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/unreblog`, data);
+ reply.code(401).send(data);
}
});
}
- public async bookmarkStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public bookmarkStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.bookmarkStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/bookmark`, data);
+ reply.code(401).send(data);
}
});
}
- public async unbookmarkStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public unbookmarkStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unbookmarkStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/unbookmark`, data);
+ reply.code(401).send(data);
}
});
}
- public async pinStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public pinStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.pinStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/pin`, data);
+ reply.code(401).send(data);
}
});
}
- public async unpinStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public unpinStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unpinStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/unpin`, data);
+ reply.code(401).send(data);
}
});
}
- public async reactStatus() {
- this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public reactStatus() {
+ this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/react/${_request.params.name}`, data);
+ reply.code(401).send(data);
}
});
}
- public async unreactStatus() {
- this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public unreactStatus() {
+ this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/unreact/${_request.params.name}`, data);
+ reply.code(401).send(data);
}
});
}
- public async deleteStatus() {
- this.fastify.delete<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public deleteStatus() {
+ this.fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.deleteStatus(_request.params.id);
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`DELETE /v1/statuses/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
index 3eb4898713..1a732d62de 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
@@ -3,270 +3,231 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { ParsedUrlQuery } from 'querystring';
-import { convertConversation, convertList, MastoConverters } from '../converters.js';
-import { getClient } from '../MastodonApiServerService.js';
+import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
+import { convertList, MastoConverters } from '../converters.js';
+import { getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
+import { parseTimelineArgs, TimelineArgs, toBoolean } from '../timelineArgs.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance } from 'fastify';
-import type { Config } from '@/config.js';
-import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-
-export function limitToInt(q: ParsedUrlQuery) {
- const object: any = q;
- if (q.limit) if (typeof q.limit === 'string') object.limit = parseInt(q.limit, 10);
- if (q.offset) if (typeof q.offset === 'string') object.offset = parseInt(q.offset, 10);
- return object;
-}
-
-export function argsToBools(q: ParsedUrlQuery) {
- // Values taken from https://docs.joinmastodon.org/client/intro/#boolean
- const toBoolean = (value: string) =>
- !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
-
- // Keys taken from:
- // - https://docs.joinmastodon.org/methods/accounts/#statuses
- // - https://docs.joinmastodon.org/methods/timelines/#public
- // - https://docs.joinmastodon.org/methods/timelines/#tag
- const object: any = q;
- if (q.only_media) if (typeof q.only_media === 'string') object.only_media = toBoolean(q.only_media);
- if (q.exclude_replies) if (typeof q.exclude_replies === 'string') object.exclude_replies = toBoolean(q.exclude_replies);
- if (q.exclude_reblogs) if (typeof q.exclude_reblogs === 'string') object.exclude_reblogs = toBoolean(q.exclude_reblogs);
- if (q.pinned) if (typeof q.pinned === 'string') object.pinned = toBoolean(q.pinned);
- if (q.local) if (typeof q.local === 'string') object.local = toBoolean(q.local);
- return q;
-}
export class ApiTimelineMastodon {
- private fastify: FastifyInstance;
-
- constructor(fastify: FastifyInstance, config: Config, private mastoconverter: MastoConverters) {
- this.fastify = fastify;
- }
+ constructor(
+ private readonly fastify: FastifyInstance,
+ private readonly mastoConverters: MastoConverters,
+ private readonly logger: MastodonLogger,
+ private readonly mastodon: MastodonApiServerService,
+ ) {}
- public async getTL() {
- this.fastify.get('/v1/timelines/public', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getTL() {
+ this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/public', async (_request, reply) => {
try {
- const query: any = _request.query;
- const data = query.local === 'true'
- ? await client.getLocalTimeline(argsToBools(limitToInt(query)))
- : await client.getPublicTimeline(argsToBools(limitToInt(query)));
- reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = toBoolean(_request.query.local)
+ ? await client.getLocalTimeline(parseTimelineArgs(_request.query))
+ : await client.getPublicTimeline(parseTimelineArgs(_request.query));
+ reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/timelines/public', data);
+ reply.code(401).send(data);
}
});
}
- public async getHomeTl() {
- this.fastify.get('/v1/timelines/home', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getHomeTl() {
+ this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (_request, reply) => {
try {
- const query: any = _request.query;
- const data = await client.getHomeTimeline(limitToInt(query));
- reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.getHomeTimeline(parseTimelineArgs(_request.query));
+ reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/timelines/home', data);
+ reply.code(401).send(data);
}
});
}
- public async getTagTl() {
- this.fastify.get<{ Params: { hashtag: string } }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getTagTl() {
+ this.fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
try {
- const query: any = _request.query;
- const params: any = _request.params;
- const data = await client.getTagTimeline(params.hashtag, limitToInt(query));
- reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ if (!_request.params.hashtag) return reply.code(400).send({ error: 'Missing required parameter "hashtag"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.getTagTimeline(_request.params.hashtag, parseTimelineArgs(_request.query));
+ reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/timelines/tag/${_request.params.hashtag}`, data);
+ reply.code(401).send(data);
}
});
}
- public async getListTL() {
- this.fastify.get<{ Params: { id: string } }>('/v1/timelines/list/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getListTL() {
+ this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (_request, reply) => {
try {
- const query: any = _request.query;
- const params: any = _request.params;
- const data = await client.getListTimeline(params.id, limitToInt(query));
- reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.getListTimeline(_request.params.id, parseTimelineArgs(_request.query));
+ reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/timelines/list/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async getConversations() {
- this.fastify.get('/v1/conversations', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getConversations() {
+ this.fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (_request, reply) => {
try {
- const query: any = _request.query;
- const data = await client.getConversationTimeline(limitToInt(query));
- reply.send(data.data.map((conversation: Entity.Conversation) => convertConversation(conversation)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.getConversationTimeline(parseTimelineArgs(_request.query));
+ const conversations = await Promise.all(data.data.map(async (conversation: Entity.Conversation) => await this.mastoConverters.convertConversation(conversation, me)));
+ reply.send(conversations);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/conversations', data);
+ reply.code(401).send(data);
}
});
}
- public async getList() {
- this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
+ public getList() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const data = await client.getList(params.id);
+ const data = await client.getList(_request.params.id);
reply.send(convertList(data.data));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/lists/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async getLists() {
+ public getLists() {
this.fastify.get('/v1/lists', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const data = await client.getLists();
reply.send(data.data.map((list: Entity.List) => convertList(list)));
- } catch (e: any) {
- console.error(e);
- return e.response.data;
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/lists', data);
+ reply.code(401).send(data);
}
});
}
- public async getListAccounts() {
- this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
+ public getListAccounts() {
+ this.fastify.get<{ Params: { id?: string }, Querystring: { limit?: number, max_id?: string, since_id?: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const query: any = _request.query;
- const data = await client.getAccountsInList(params.id, query);
- reply.send(data.data.map((account: Entity.Account) => this.mastoconverter.convertAccount(account)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ const data = await client.getAccountsInList(_request.params.id, _request.query);
+ const accounts = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
+ reply.send(accounts);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/lists/${_request.params.id}/accounts`, data);
+ reply.code(401).send(data);
}
});
}
- public async addListAccount() {
- this.fastify.post<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
+ public addListAccount() {
+ this.fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const query: any = _request.query;
- const data = await client.addAccountsToList(params.id, query.accounts_id);
+ const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/lists/${_request.params.id}/accounts`, data);
+ reply.code(401).send(data);
}
});
}
- public async rmListAccount() {
- this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
+ public rmListAccount() {
+ this.fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const query: any = _request.query;
- const data = await client.deleteAccountsFromList(params.id, query.accounts_id);
+ const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`DELETE /v1/lists/${_request.params.id}/accounts`, data);
+ reply.code(401).send(data);
}
});
}
- public async createList() {
- this.fastify.post('/v1/lists', async (_request, reply) => {
+ public createList() {
+ this.fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const body: any = _request.body;
- const data = await client.createList(body.title);
+ const data = await client.createList(_request.body.title);
reply.send(convertList(data.data));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/lists', data);
+ reply.code(401).send(data);
}
});
}
- public async updateList() {
- this.fastify.put<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
+ public updateList() {
+ this.fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const body: any = _request.body;
- const params: any = _request.params;
- const data = await client.updateList(params.id, body.title);
+ const data = await client.updateList(_request.params.id, _request.body.title);
reply.send(convertList(data.data));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`PUT /v1/lists/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async deleteList() {
- this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
+ public deleteList() {
+ this.fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const data = await client.deleteList(params.id);
+ await client.deleteList(_request.params.id);
reply.send({});
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`DELETE /v1/lists/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
diff --git a/packages/backend/src/server/api/mastodon/timelineArgs.ts b/packages/backend/src/server/api/mastodon/timelineArgs.ts
new file mode 100644
index 0000000000..3fba8ec57a
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/timelineArgs.ts
@@ -0,0 +1,47 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// Keys taken from:
+// - https://docs.joinmastodon.org/methods/accounts/#statuses
+// - https://docs.joinmastodon.org/methods/timelines/#public
+// - https://docs.joinmastodon.org/methods/timelines/#tag
+export interface TimelineArgs {
+ max_id?: string;
+ min_id?: string;
+ since_id?: string;
+ limit?: string;
+ offset?: string;
+ local?: string;
+ pinned?: string;
+ exclude_reblogs?: string;
+ exclude_replies?: string;
+ only_media?: string;
+}
+
+// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
+export function toBoolean(value: string | undefined): boolean | undefined {
+ if (value === undefined) return undefined;
+ return !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
+}
+
+export function toInt(value: string | undefined): number | undefined {
+ if (value === undefined) return undefined;
+ return parseInt(value);
+}
+
+export function parseTimelineArgs(q: TimelineArgs) {
+ return {
+ max_id: q.max_id,
+ min_id: q.min_id,
+ since_id: q.since_id,
+ limit: typeof(q.limit) === 'string' ? parseInt(q.limit, 10) : undefined,
+ offset: typeof(q.offset) === 'string' ? parseInt(q.offset, 10) : undefined,
+ local: typeof(q.local) === 'string' ? toBoolean(q.local) : undefined,
+ pinned: typeof(q.pinned) === 'string' ? toBoolean(q.pinned) : undefined,
+ exclude_reblogs: typeof(q.exclude_reblogs) === 'string' ? toBoolean(q.exclude_reblogs) : undefined,
+ exclude_replies: typeof(q.exclude_replies) === 'string' ? toBoolean(q.exclude_replies) : undefined,
+ only_media: typeof(q.only_media) === 'string' ? toBoolean(q.only_media) : undefined,
+ };
+}
diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts
index eb854a7141..c80dda8d96 100644
--- a/packages/backend/src/server/api/openapi/schemas.ts
+++ b/packages/backend/src/server/api/openapi/schemas.ts
@@ -3,13 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { deepClone } from '@/misc/clone.js';
import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res', includeSelfRef: boolean): any {
// optional, nullable, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { optional, nullable, ref, selfRef, ...res }: any = schema;
+ const { optional, nullable, ref, selfRef, ..._res }: any = schema;
+ const res = deepClone(_res);
if (schema.type === 'object' && schema.properties) {
if (type === 'res') {
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index f102cb42e1..e98e2a2f3f 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -16,11 +16,11 @@ import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import type Logger from '@/logger.js';
import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
-import { LoggerService } from '@/core/LoggerService.js';
-import type Logger from '@/logger.js';
const MAX_CHANNELS_PER_CONNECTION = 32;
@@ -45,8 +45,8 @@ export default class Connection {
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
public userMutedInstances: Set<string> = new Set();
private fetchIntervalId: NodeJS.Timeout | null = null;
- private activeRateLimitRequests: number = 0;
- private closingConnection: boolean = false;
+ private activeRateLimitRequests = 0;
+ private closingConnection = false;
private logger: Logger;
constructor(
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index ae9c7e3e99..7a6193ccfc 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -6,9 +6,10 @@
import { bindThis } from '@/decorators.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
-import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import { isRenotePacked, isQuotePacked, isPackedPureRenote } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import type Connection from './Connection.js';
/**
@@ -16,6 +17,7 @@ import type Connection from './Connection.js';
*/
// eslint-disable-next-line import/no-default-export
export default abstract class Channel {
+ protected readonly noteEntityService: NoteEntityService;
protected connection: Connection;
public id: string;
public abstract readonly chName: string;
@@ -74,12 +76,29 @@ export default abstract class Channel {
// 流れてきたNoteがリノートをミュートしてるユーザが行ったもの
if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true;
+ // If it's a boost (pure renote) then we need to check the target as well
+ if (isPackedPureRenote(note) && note.renote && this.isNoteMutedOrBlocked(note.renote)) return true;
+
return false;
}
- constructor(id: string, connection: Connection) {
+ protected async hideNote(note: Packed<'Note'>): Promise<void> {
+ if (note.renote) {
+ await this.hideNote(note.renote);
+ }
+
+ if (note.reply) {
+ await this.hideNote(note.reply);
+ }
+
+ const meId = this.user?.id ?? null;
+ await this.noteEntityService.hideNote(note, meId);
+ }
+
+ constructor(id: string, connection: Connection, noteEntityService: NoteEntityService) {
this.id = id;
this.connection = connection;
+ this.noteEntityService = noteEntityService;
}
public send(payload: { type: string, body: JsonValue }): void
@@ -101,6 +120,44 @@ export default abstract class Channel {
public dispose?(): void;
public onMessage?(type: string, body: JsonValue): void;
+
+ public async assignMyReaction(note: Packed<'Note'>): Promise<Packed<'Note'>> {
+ let changed = false;
+ // StreamingApiServerService creates a single EventEmitter per server process,
+ // so a new note arriving from redis gets de-serialised once per server process,
+ // and then that single object is passed to all active channels on each connection.
+ // If we didn't clone the notes here, different connections would asynchronously write
+ // different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar
+ const clonedNote = { ...note };
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
+ if (note.renote && Object.keys(note.renote.reactions).length > 0) {
+ const myReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
+ if (myReaction) {
+ changed = true;
+ clonedNote.renote = { ...note.renote };
+ clonedNote.renote.myReaction = myReaction;
+ }
+ }
+ if (note.renote?.reply && Object.keys(note.renote.reply.reactions).length > 0) {
+ const myReaction = await this.noteEntityService.populateMyReaction(note.renote.reply, this.user.id);
+ if (myReaction) {
+ changed = true;
+ clonedNote.renote = { ...note.renote };
+ clonedNote.renote.reply = { ...note.renote.reply };
+ clonedNote.renote.reply.myReaction = myReaction;
+ }
+ }
+ }
+ if (this.user && note.reply && Object.keys(note.reply.reactions).length > 0) {
+ const myReaction = await this.noteEntityService.populateMyReaction(note.reply, this.user.id);
+ if (myReaction) {
+ changed = true;
+ clonedNote.reply = { ...note.reply };
+ clonedNote.reply.myReaction = myReaction;
+ }
+ }
+ return changed ? clonedNote : note;
+ }
}
export type MiChannelService<T extends boolean> = {
diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts
index 355d5dba21..a0140d395d 100644
--- a/packages/backend/src/server/api/stream/channels/admin.ts
+++ b/packages/backend/src/server/api/stream/channels/admin.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
class AdminChannel extends Channel {
@@ -30,6 +31,7 @@ export class AdminChannelService implements MiChannelService<true> {
public readonly kind = AdminChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -38,6 +40,7 @@ export class AdminChannelService implements MiChannelService<true> {
return new AdminChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 53dc7f18b6..a73d158b7f 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -18,12 +18,12 @@ class AntennaChannel extends Channel {
private antennaId: string;
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onEvent = this.onEvent.bind(this);
}
diff --git a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
index 8693f0c6ac..5ebbdcbb86 100644
--- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
@@ -3,8 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
-import { DI } from '@/di-symbols.js';
+import { Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -27,13 +26,12 @@ class BubbleTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
private roleService: RoleService,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
- //this.onNote = this.onNote.bind(this);
+ super(id, connection, noteEntityService);
}
@bindThis
@@ -65,16 +63,12 @@ class BubbleTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 9939aa49ee..ec0bc7e13a 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -7,7 +7,6 @@ import { Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
-import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@@ -20,12 +19,12 @@ class ChannelChannel extends Channel {
private withRenotes: boolean;
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -50,16 +49,12 @@ class ChannelChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts
index 03768f3d23..fa097fdc35 100644
--- a/packages/backend/src/server/api/stream/channels/drive.ts
+++ b/packages/backend/src/server/api/stream/channels/drive.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
class DriveChannel extends Channel {
@@ -30,6 +31,7 @@ export class DriveChannelService implements MiChannelService<true> {
public readonly kind = DriveChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -38,6 +40,7 @@ export class DriveChannelService implements MiChannelService<true> {
return new DriveChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
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 6fe76747ee..72a8a8b156 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -24,12 +24,12 @@ class GlobalTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
private roleService: RoleService,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -60,16 +60,12 @@ class GlobalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index 8105f15cb1..7c8df87721 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -8,7 +8,6 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
-import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@@ -19,12 +18,12 @@ class HashtagChannel extends Channel {
private q: string[][];
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -46,16 +45,12 @@ class HashtagChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
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 359ab3e223..c87a21be82 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -20,12 +20,12 @@ class HomeTimelineChannel extends Channel {
private withFiles: boolean;
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -81,16 +81,12 @@ class HomeTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
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 01645fe657..95b762e2b7 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -26,12 +26,12 @@ class HybridTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
private roleService: RoleService,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -98,17 +98,12 @@ class HybridTimelineChannel extends Channel {
}
}
- if (this.user && note.renoteId && !note.text) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- console.log(note.renote.reactionAndUserPairCache);
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
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 1f9d25b44d..b9e0a4c234 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -25,12 +25,12 @@ class LocalTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
private roleService: RoleService,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -70,16 +70,12 @@ class LocalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts
index 863d7f4c4e..6b144e43e4 100644
--- a/packages/backend/src/server/api/stream/channels/main.ts
+++ b/packages/backend/src/server/api/stream/channels/main.ts
@@ -17,12 +17,12 @@ class MainChannel extends Channel {
public static kind = 'read:account';
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts
index 91b62255b4..a4006ab752 100644
--- a/packages/backend/src/server/api/stream/channels/queue-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev();
@@ -17,8 +18,8 @@ class QueueStatsChannel extends Channel {
public static shouldShare = true;
public static requireCredential = false as const;
- constructor(id: string, connection: Channel['connection']) {
- super(id, connection);
+ constructor(id: string, connection: Channel['connection'], noteEntityService: NoteEntityService) {
+ super(id, connection, noteEntityService);
//this.onStats = this.onStats.bind(this);
//this.onMessage = this.onMessage.bind(this);
}
@@ -64,6 +65,7 @@ export class QueueStatsChannelService implements MiChannelService<false> {
public readonly kind = QueueStatsChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -72,6 +74,7 @@ export class QueueStatsChannelService implements MiChannelService<false> {
return new QueueStatsChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts
index 7597a1cfa3..b7fffbe844 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -11,6 +11,7 @@ import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
import { reversiUpdateKeys } from 'misskey-js';
@@ -23,11 +24,12 @@ class ReversiGameChannel extends Channel {
constructor(
private reversiService: ReversiService,
private reversiGameEntityService: ReversiGameEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
}
@bindThis
@@ -116,6 +118,7 @@ export class ReversiGameChannelService implements MiChannelService<false> {
constructor(
private reversiService: ReversiService,
private reversiGameEntityService: ReversiGameEntityService,
+ private noteEntityService: NoteEntityService,
) {
}
@@ -124,6 +127,7 @@ export class ReversiGameChannelService implements MiChannelService<false> {
return new ReversiGameChannel(
this.reversiService,
this.reversiGameEntityService,
+ this.noteEntityService,
id,
connection,
);
diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts
index 6e88939724..dc73d3a3d8 100644
--- a/packages/backend/src/server/api/stream/channels/reversi.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
class ReversiChannel extends Channel {
@@ -17,8 +18,9 @@ class ReversiChannel extends Channel {
constructor(
id: string,
connection: Channel['connection'],
+ noteEntityService: NoteEntityService,
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
}
@bindThis
@@ -40,6 +42,7 @@ export class ReversiChannelService implements MiChannelService<true> {
public readonly kind = ReversiChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -48,6 +51,7 @@ export class ReversiChannelService implements MiChannelService<true> {
return new ReversiChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
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 fcfa26c38b..14c4d96479 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -18,13 +18,13 @@ class RoleTimelineChannel extends Channel {
private roleId: string;
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
private roleservice: RoleService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -48,7 +48,12 @@ class RoleTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- this.send('note', note);
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
+
+ this.connection.cacheNote(clonedNote);
+
+ this.send('note', clonedNote);
} else {
this.send(data.type, data.body);
}
diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts
index ec5352d12d..43cbf65110 100644
--- a/packages/backend/src/server/api/stream/channels/server-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/server-stats.ts
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev();
@@ -17,8 +18,8 @@ class ServerStatsChannel extends Channel {
public static shouldShare = true;
public static requireCredential = false as const;
- constructor(id: string, connection: Channel['connection']) {
- super(id, connection);
+ constructor(id: string, connection: Channel['connection'], noteEntityService: NoteEntityService) {
+ super(id, connection, noteEntityService);
//this.onStats = this.onStats.bind(this);
//this.onMessage = this.onMessage.bind(this);
}
@@ -62,6 +63,7 @@ export class ServerStatsChannelService implements MiChannelService<false> {
public readonly kind = ServerStatsChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -70,6 +72,7 @@ export class ServerStatsChannelService implements MiChannelService<false> {
return new ServerStatsChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 4f38351e94..d09a9b8d9f 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -26,12 +26,12 @@ class UserListChannel extends Channel {
constructor(
private userListsRepository: UserListsRepository,
private userListMembershipsRepository: UserListMembershipsRepository,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.updateListUsers = this.updateListUsers.bind(this);
//this.onNote = this.onNote.bind(this);
}
@@ -111,16 +111,12 @@ class UserListChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index e59314bf55..3ed811e737 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -325,14 +325,14 @@ export class ClientServerService {
} else {
const port = (process.env.VITE_PORT ?? '5173');
fastify.register(fastifyProxy, {
- upstream: 'http://localhost:' + port,
+ upstream: `http://localhost:${port}`,
prefix: '/vite',
rewritePrefix: '/vite',
});
const embedPort = (process.env.EMBED_VITE_PORT ?? '5174');
fastify.register(fastifyProxy, {
- upstream: 'http://localhost:' + embedPort,
+ upstream: `http://localhost:${embedPort}`,
prefix: '/embed_vite',
rewritePrefix: '/embed_vite',
});
@@ -488,7 +488,12 @@ export class ClientServerService {
});
fastify.get('/robots.txt', async (request, reply) => {
- return await reply.sendFile('/robots.txt', staticAssets);
+ if (this.meta.robotsTxt) {
+ reply.header('Content-Type', 'text/plain');
+ return await reply.send(this.meta.robotsTxt);
+ } else {
+ return await reply.sendFile('/robots.txt', staticAssets);
+ }
});
// OpenSearch XML
@@ -531,6 +536,7 @@ export class ClientServerService {
host: host ?? IsNull(),
isSuspended: false,
enableRss: true,
+ requireSigninToViewContents: false,
});
return user && await this.feedService.packFeed(user);
@@ -835,6 +841,7 @@ export class ClientServerService {
fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
const announcement = await this.announcementsRepository.findOneBy({
id: request.params.announcementId,
+ userId: IsNull(),
});
if (announcement) {
diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js
index b07dce3ac4..1af1dc545b 100644
--- a/packages/backend/src/server/web/boot.embed.js
+++ b/packages/backend/src/server/web/boot.embed.js
@@ -48,7 +48,7 @@
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
- lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
// Fallback
if (lang == null) lang = 'en-US';
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index bf83340bde..54750e26e5 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -39,7 +39,7 @@
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
- lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
// Fallback
if (lang == null) lang = 'en-US';
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 37bed27fb1..2c6ef731b8 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -58,6 +58,8 @@ export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
export const followingVisibilities = ['public', 'followers', 'private'] as const;
export const followersVisibilities = ['public', 'followers', 'private'] as const;
+export const defaultCWPriorities = ['default', 'parent', 'defaultParent', 'parentDefault'] as const;
+
/**
* ユーザーがエクスポートできるものの種類
*
@@ -98,6 +100,7 @@ export const moderationLogTypes = [
'deleteGlobalAnnouncement',
'deleteUserAnnouncement',
'resetPassword',
+ 'setMandatoryCW',
'setRemoteInstanceNSFW',
'unsetRemoteInstanceNSFW',
'suspendRemoteInstance',
@@ -129,6 +132,25 @@ export const moderationLogTypes = [
'deletePage',
'deleteFlash',
'deleteGalleryPost',
+ 'acceptQuotesUser',
+ 'rejectQuotesUser',
+ 'acceptQuotesInstance',
+ 'rejectQuotesInstance',
+ 'clearUserFiles',
+ 'nsfwUser',
+ 'unNsfwUser',
+ 'silenceUser',
+ 'unSilenceUser',
+ 'createAccount',
+ 'clearRemoteFiles',
+ 'clearOwnerlessFiles',
+ 'updateCustomEmojis',
+ 'importCustomEmojis',
+ 'clearInstanceFiles',
+ 'severFollowRelations',
+ 'createPromo',
+ 'addRelay',
+ 'removeRelay',
] as const;
export type ModerationLogPayloads = {
@@ -217,7 +239,6 @@ export type ModerationLogPayloads = {
noteUserId: string;
noteUserUsername: string;
noteUserHost: string | null;
- note: any;
};
createGlobalAnnouncement: {
announcementId: string;
@@ -259,6 +280,13 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
+ setMandatoryCW: {
+ newCW: string | null;
+ oldCW: string | null;
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
setRemoteInstanceNSFW: {
id: string;
host: string;
@@ -393,20 +421,99 @@ export type ModerationLogPayloads = {
pageId: string;
pageUserId: string;
pageUserUsername: string;
- page: any;
};
deleteFlash: {
flashId: string;
flashUserId: string;
flashUserUsername: string;
- flash: any;
};
deleteGalleryPost: {
postId: string;
postUserId: string;
postUserUsername: string;
- post: any;
};
+ acceptQuotesUser: {
+ userId: string,
+ userUsername: string,
+ userHost: string | null,
+ };
+ rejectQuotesUser: {
+ userId: string,
+ userUsername: string,
+ userHost: string | null,
+ };
+ acceptQuotesInstance: {
+ id: string;
+ host: string;
+ };
+ rejectQuotesInstance: {
+ id: string;
+ host: string;
+ };
+ clearUserFiles: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ count: number;
+ };
+ nsfwUser: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ unNsfwUser: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ silenceUser: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ unSilenceUser: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ createAccount: {
+ userId: string;
+ userUsername: string;
+ };
+ clearRemoteFiles: Record<string, never>;
+ clearOwnerlessFiles: {
+ count: number;
+ };
+ updateCustomEmojis: {
+ ids: string[],
+ category?: string | null,
+ license?: string | null,
+ setAliases?: string[],
+ addAliases?: string[],
+ delAliases?: string[],
+ },
+ importCustomEmojis: {
+ fileName: string,
+ },
+ clearInstanceFiles: {
+ host: string;
+ count: number;
+ },
+ severFollowRelations: {
+ host: string;
+ },
+ createPromo: {
+ noteId: string,
+ noteUserId: string;
+ noteUserUsername: string;
+ noteUserHost: string | null;
+ },
+ addRelay: {
+ inbox: string;
+ },
+ removeRelay: {
+ inbox: string;
+ },
};
export type Serialized<T> = {
diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml
index 8c38f16919..8b270e58f7 100644
--- a/packages/backend/test-federation/compose.tpl.yml
+++ b/packages/backend/test-federation/compose.tpl.yml
@@ -17,6 +17,7 @@ services:
- ./.config/docker.env
environment:
- NODE_ENV=production
+ - COREPACK_DEFAULT_TO_LATEST=0
volumes:
- type: bind
source: ../../../built
diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml
index 62d7e977c0..a5a7223982 100644
--- a/packages/backend/test-federation/compose.yml
+++ b/packages/backend/test-federation/compose.yml
@@ -25,6 +25,7 @@ services:
environment:
- NODE_ENV=development
- NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
+ - COREPACK_DEFAULT_TO_LATEST=0
volumes:
- type: bind
source: ../package.json
@@ -85,6 +86,8 @@ services:
depends_on:
redis.test:
condition: service_healthy
+ environment:
+ - COREPACK_DEFAULT_TO_LATEST=0
volumes:
- type: bind
source: ../package.json
diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts
index bacc4cc54f..220c22e198 100644
--- a/packages/backend/test-federation/test/note.test.ts
+++ b/packages/backend/test-federation/test/note.test.ts
@@ -131,11 +131,7 @@ describe('Note', () => {
rejects(
async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }),
(err: any) => {
- /**
- * FIXME: this error is not handled
- * @see https://github.com/misskey-dev/misskey/issues/12736
- */
- strictEqual(err.code, 'INTERNAL_ERROR');
+ strictEqual(err.code, 'REQUEST_FAILED');
return true;
},
);
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index d12be2a9ac..319c8581f4 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -397,7 +397,7 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
- }, 1000 * 10);
+ }, 1000 * 15);
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts
index c8f3db8aac..f11097b986 100644
--- a/packages/backend/test/misc/mock-resolver.ts
+++ b/packages/backend/test/misc/mock-resolver.ts
@@ -23,6 +23,8 @@ import type {
PollsRepository,
UsersRepository,
} from '@/models/_.js';
+import { ApLogService } from '@/core/ApLogService.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
type MockResponse = {
type: string;
@@ -49,6 +51,8 @@ export class MockResolver extends Resolver {
{} as ApRendererService,
{} as ApDbResolverService,
loggerService,
+ {} as ApLogService,
+ {} as ApUtilityService,
);
}
diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts
index 235af29f0d..1326003c5e 100644
--- a/packages/backend/test/unit/AbuseReportNotificationService.ts
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -3,13 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { jest } from '@jest/globals';
+import { describe, jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import { randomString } from '../utils.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import {
AbuseReportNotificationRecipientRepository,
MiAbuseReportNotificationRecipient,
+ MiAbuseUserReport,
MiSystemWebhook,
MiUser,
SystemWebhooksRepository,
@@ -112,7 +113,10 @@ describe('AbuseReportNotificationService', () => {
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
},
{
- provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
+ provide: UserEntityService, useFactory: () => ({
+ pack: (v: any) => Promise.resolve(v),
+ packMany: (v: any) => Promise.resolve(v),
+ }),
},
{
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
@@ -344,4 +348,46 @@ describe('AbuseReportNotificationService', () => {
expect(recipients).toEqual([recipient3]);
});
});
+
+ describe('notifySystemWebhook', () => {
+ test('非アクティブな通報通知はWebhook送信から除外される', async () => {
+ const recipient1 = await createRecipient({
+ method: 'webhook',
+ systemWebhookId: systemWebhook1.id,
+ isActive: true,
+ });
+ const recipient2 = await createRecipient({
+ method: 'webhook',
+ systemWebhookId: systemWebhook2.id,
+ isActive: false,
+ });
+
+ const reports: MiAbuseUserReport[] = [
+ {
+ id: idService.gen(),
+ targetUserId: alice.id,
+ targetUser: alice,
+ reporterId: bob.id,
+ reporter: bob,
+ assigneeId: null,
+ assignee: null,
+ resolved: false,
+ forwarded: false,
+ comment: 'test',
+ moderationNote: '',
+ resolvedAs: null,
+ targetUserHost: null,
+ reporterHost: null,
+ },
+ ];
+
+ await service.notifySystemWebhook(reports, 'abuseReport');
+
+ // 実際に除外されるかはSystemWebhookService側で確認する.
+ // ここでは非アクティブな通報通知を除外設定できているかを確認する
+ expect(webhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
+ expect(webhookService.enqueueSystemWebhook.mock.calls[0][0]).toBe('abuseReport');
+ expect(webhookService.enqueueSystemWebhook.mock.calls[0][2]).toEqual({ excludes: [systemWebhook2.id] });
+ });
+ });
});
diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts
new file mode 100644
index 0000000000..94a743e6b8
--- /dev/null
+++ b/packages/backend/test/unit/CaptchaService.ts
@@ -0,0 +1,639 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { afterAll, beforeAll, beforeEach, describe, expect, jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { Response } from 'node-fetch';
+import {
+ CaptchaError,
+ CaptchaErrorCode,
+ captchaErrorCodes,
+ CaptchaSaveResult,
+ CaptchaService,
+} from '@/core/CaptchaService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { HttpRequestService } from '@/core/HttpRequestService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { MiMeta } from '@/models/Meta.js';
+import { LoggerService } from '@/core/LoggerService.js';
+
+describe('CaptchaService', () => {
+ let app: TestingModule;
+ let service: CaptchaService;
+ let httpRequestService: jest.Mocked<HttpRequestService>;
+ let metaService: jest.Mocked<MetaService>;
+
+ beforeAll(async () => {
+ app = await Test.createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ CaptchaService,
+ LoggerService,
+ {
+ provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }),
+ },
+ {
+ provide: MetaService, useFactory: () => ({
+ fetch: jest.fn(),
+ update: jest.fn(),
+ }),
+ },
+ ],
+ }).compile();
+
+ app.enableShutdownHooks();
+
+ service = app.get(CaptchaService);
+ httpRequestService = app.get(HttpRequestService) as jest.Mocked<HttpRequestService>;
+ metaService = app.get(MetaService) as jest.Mocked<MetaService>;
+ });
+
+ beforeEach(() => {
+ httpRequestService.send.mockClear();
+ metaService.update.mockClear();
+ metaService.fetch.mockClear();
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ function successMock(result: object) {
+ httpRequestService.send.mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: async () => (result),
+ } as Response);
+ }
+
+ function failureHttpMock() {
+ httpRequestService.send.mockResolvedValue({
+ ok: false,
+ status: 400,
+ } as Response);
+ }
+
+ function failureVerificationMock(result: object) {
+ httpRequestService.send.mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: async () => (result),
+ } as Response);
+ }
+
+ async function testCaptchaError(code: CaptchaErrorCode, test: () => Promise<void>) {
+ try {
+ await test();
+ expect(false).toBe(true);
+ } catch (e) {
+ expect(e instanceof CaptchaError).toBe(true);
+
+ const _e = e as CaptchaError;
+ expect(_e.code).toBe(code);
+ }
+ }
+
+ describe('verifyRecaptcha', () => {
+ test('success', async () => {
+ successMock({ success: true });
+ await service.verifyRecaptcha('secret', 'response');
+ });
+
+ test('noResponseProvided', async () => {
+ await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyRecaptcha('secret', null));
+ });
+
+ test('requestFailed', async () => {
+ failureHttpMock();
+ await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyRecaptcha('secret', 'response'));
+ });
+
+ test('verificationFailed', async () => {
+ failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
+ await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyRecaptcha('secret', 'response'));
+ });
+ });
+
+ describe('verifyHcaptcha', () => {
+ test('success', async () => {
+ successMock({ success: true });
+ await service.verifyHcaptcha('secret', 'response');
+ });
+
+ test('noResponseProvided', async () => {
+ await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyHcaptcha('secret', null));
+ });
+
+ test('requestFailed', async () => {
+ failureHttpMock();
+ await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyHcaptcha('secret', 'response'));
+ });
+
+ test('verificationFailed', async () => {
+ failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
+ await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyHcaptcha('secret', 'response'));
+ });
+ });
+
+ describe('verifyMcaptcha', () => {
+ const host = 'https://localhost';
+
+ test('success', async () => {
+ successMock({ valid: true });
+ await service.verifyMcaptcha('secret', 'sitekey', host, 'response');
+ });
+
+ test('noResponseProvided', async () => {
+ await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyMcaptcha('secret', 'sitekey', host, null));
+ });
+
+ test('requestFailed', async () => {
+ failureHttpMock();
+ await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response'));
+ });
+
+ test('verificationFailed', async () => {
+ failureVerificationMock({ valid: false });
+ await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response'));
+ });
+ });
+
+ describe('verifyTurnstile', () => {
+ test('success', async () => {
+ successMock({ success: true });
+ await service.verifyTurnstile('secret', 'response');
+ });
+
+ test('noResponseProvided', async () => {
+ await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTurnstile('secret', null));
+ });
+
+ test('requestFailed', async () => {
+ failureHttpMock();
+ await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyTurnstile('secret', 'response'));
+ });
+
+ test('verificationFailed', async () => {
+ failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
+ await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTurnstile('secret', 'response'));
+ });
+ });
+
+ describe('verifyTestcaptcha', () => {
+ test('success', async () => {
+ await service.verifyTestcaptcha('testcaptcha-passed');
+ });
+
+ test('noResponseProvided', async () => {
+ await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTestcaptcha(null));
+ });
+
+ test('verificationFailed', async () => {
+ await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTestcaptcha('testcaptcha-failed'));
+ });
+ });
+
+ describe('get', () => {
+ function setupMeta(meta: Partial<MiMeta>) {
+ metaService.fetch.mockResolvedValue(meta as MiMeta);
+ }
+
+ test('values', async () => {
+ setupMeta({
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ fcSiteKey: 'fc-sitekey',
+ fcSecretKey: 'fc-secret',
+ hcaptchaSiteKey: 'hcaptcha-sitekey',
+ hcaptchaSecretKey: 'hcaptcha-secret',
+ mcaptchaSitekey: 'mcaptcha-sitekey',
+ mcaptchaSecretKey: 'mcaptcha-secret',
+ mcaptchaInstanceUrl: 'https://localhost',
+ recaptchaSiteKey: 'recaptcha-sitekey',
+ recaptchaSecretKey: 'recaptcha-secret',
+ turnstileSiteKey: 'turnstile-sitekey',
+ turnstileSecretKey: 'turnstile-secret',
+ });
+
+ const result = await service.get();
+ expect(result.provider).toBe('none');
+ expect(result.fc.siteKey).toBe('fc-sitekey');
+ expect(result.fc.secretKey).toBe('fc-secret');
+ expect(result.hcaptcha.siteKey).toBe('hcaptcha-sitekey');
+ expect(result.hcaptcha.secretKey).toBe('hcaptcha-secret');
+ expect(result.mcaptcha.siteKey).toBe('mcaptcha-sitekey');
+ expect(result.mcaptcha.secretKey).toBe('mcaptcha-secret');
+ expect(result.mcaptcha.instanceUrl).toBe('https://localhost');
+ expect(result.recaptcha.siteKey).toBe('recaptcha-sitekey');
+ expect(result.recaptcha.secretKey).toBe('recaptcha-secret');
+ expect(result.turnstile.siteKey).toBe('turnstile-sitekey');
+ expect(result.turnstile.secretKey).toBe('turnstile-secret');
+ });
+
+ describe('provider', () => {
+ test('none', async () => {
+ setupMeta({
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ });
+
+ const result = await service.get();
+ expect(result.provider).toBe('none');
+ });
+
+ test('hcaptcha', async () => {
+ setupMeta({
+ enableFC: false,
+ enableHcaptcha: true,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ });
+
+ const result = await service.get();
+ expect(result.provider).toBe('hcaptcha');
+ });
+
+ test('mcaptcha', async () => {
+ setupMeta({
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: true,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ });
+
+ const result = await service.get();
+ expect(result.provider).toBe('mcaptcha');
+ });
+
+ test('recaptcha', async () => {
+ setupMeta({
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: true,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ });
+
+ const result = await service.get();
+ expect(result.provider).toBe('recaptcha');
+ });
+
+ test('turnstile', async () => {
+ setupMeta({
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: true,
+ enableTestcaptcha: false,
+ });
+
+ const result = await service.get();
+ expect(result.provider).toBe('turnstile');
+ });
+
+ test('testcaptcha', async () => {
+ setupMeta({
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: true,
+ });
+
+ const result = await service.get();
+ expect(result.provider).toBe('testcaptcha');
+ });
+ });
+ });
+
+ describe('save', () => {
+ const host = 'https://localhost';
+
+ describe('[success] 検証に成功した時だけ保存できる+他のプロバイダの設定値を誤って更新しない', () => {
+ beforeEach(() => {
+ successMock({ success: true, valid: true });
+ });
+
+ async function assertSuccess(promise: Promise<CaptchaSaveResult>, expectMeta: Partial<MiMeta>) {
+ await expect(promise)
+ .resolves
+ .toStrictEqual({ success: true });
+ const partialParams = metaService.update.mock.calls[0][0];
+ expect(partialParams).toStrictEqual(expectMeta);
+ }
+
+ test('none', async () => {
+ await assertSuccess(
+ service.save('none'),
+ {
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ },
+ );
+ });
+
+ test('hcaptcha', async () => {
+ await assertSuccess(
+ service.save('hcaptcha', {
+ sitekey: 'hcaptcha-sitekey',
+ secret: 'hcaptcha-secret',
+ captchaResult: 'hcaptcha-passed',
+ }),
+ {
+ enableFC: false,
+ enableHcaptcha: true,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ hcaptchaSiteKey: 'hcaptcha-sitekey',
+ hcaptchaSecretKey: 'hcaptcha-secret',
+ },
+ );
+ });
+
+ test('mcaptcha', async () => {
+ await assertSuccess(
+ service.save('mcaptcha', {
+ sitekey: 'mcaptcha-sitekey',
+ secret: 'mcaptcha-secret',
+ instanceUrl: host,
+ captchaResult: 'mcaptcha-passed',
+ }),
+ {
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: true,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ mcaptchaSitekey: 'mcaptcha-sitekey',
+ mcaptchaSecretKey: 'mcaptcha-secret',
+ mcaptchaInstanceUrl: host,
+ },
+ );
+ });
+
+ test('recaptcha', async () => {
+ await assertSuccess(
+ service.save('recaptcha', {
+ sitekey: 'recaptcha-sitekey',
+ secret: 'recaptcha-secret',
+ captchaResult: 'recaptcha-passed',
+ }),
+ {
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: true,
+ enableTurnstile: false,
+ enableTestcaptcha: false,
+ recaptchaSiteKey: 'recaptcha-sitekey',
+ recaptchaSecretKey: 'recaptcha-secret',
+ },
+ );
+ });
+
+ test('turnstile', async () => {
+ await assertSuccess(
+ service.save('turnstile', {
+ sitekey: 'turnstile-sitekey',
+ secret: 'turnstile-secret',
+ captchaResult: 'turnstile-passed',
+ }),
+ {
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: true,
+ enableTestcaptcha: false,
+ turnstileSiteKey: 'turnstile-sitekey',
+ turnstileSecretKey: 'turnstile-secret',
+ },
+ );
+ });
+
+ test('testcaptcha', async () => {
+ await assertSuccess(
+ service.save('testcaptcha', {
+ sitekey: 'testcaptcha-sitekey',
+ secret: 'testcaptcha-secret',
+ captchaResult: 'testcaptcha-passed',
+ }),
+ {
+ enableFC: false,
+ enableHcaptcha: false,
+ enableMcaptcha: false,
+ enableRecaptcha: false,
+ enableTurnstile: false,
+ enableTestcaptcha: true,
+ },
+ );
+ });
+ });
+
+ describe('[failure] 検証に失敗した場合は保存できない+設定値の更新そのものが発生しない', () => {
+ async function assertFailure(code: CaptchaErrorCode, promise: Promise<CaptchaSaveResult>) {
+ const res = await promise;
+ expect(res.success).toBe(false);
+ if (!res.success) {
+ expect(res.error.code).toBe(code);
+ }
+ expect(metaService.update).not.toBeCalled();
+ }
+
+ describe('invalidParameters', () => {
+ test('hcaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.invalidParameters,
+ service.save('hcaptcha', {
+ sitekey: 'hcaptcha-sitekey',
+ secret: 'hcaptcha-secret',
+ captchaResult: null,
+ }),
+ );
+ });
+
+ test('mcaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.invalidParameters,
+ service.save('mcaptcha', {
+ sitekey: 'mcaptcha-sitekey',
+ secret: 'mcaptcha-secret',
+ instanceUrl: host,
+ captchaResult: null,
+ }),
+ );
+ });
+
+ test('recaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.invalidParameters,
+ service.save('recaptcha', {
+ sitekey: 'recaptcha-sitekey',
+ secret: 'recaptcha-secret',
+ captchaResult: null,
+ }),
+ );
+ });
+
+ test('turnstile', async () => {
+ await assertFailure(
+ captchaErrorCodes.invalidParameters,
+ service.save('turnstile', {
+ sitekey: 'turnstile-sitekey',
+ secret: 'turnstile-secret',
+ captchaResult: null,
+ }),
+ );
+ });
+
+ test('testcaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.invalidParameters,
+ service.save('testcaptcha', {
+ captchaResult: null,
+ }),
+ );
+ });
+ });
+
+ describe('requestFailed', () => {
+ beforeEach(() => {
+ failureHttpMock();
+ });
+
+ test('hcaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.requestFailed,
+ service.save('hcaptcha', {
+ sitekey: 'hcaptcha-sitekey',
+ secret: 'hcaptcha-secret',
+ captchaResult: 'hcaptcha-passed',
+ }),
+ );
+ });
+
+ test('mcaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.requestFailed,
+ service.save('mcaptcha', {
+ sitekey: 'mcaptcha-sitekey',
+ secret: 'mcaptcha-secret',
+ instanceUrl: host,
+ captchaResult: 'mcaptcha-passed',
+ }),
+ );
+ });
+
+ test('recaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.requestFailed,
+ service.save('recaptcha', {
+ sitekey: 'recaptcha-sitekey',
+ secret: 'recaptcha-secret',
+ captchaResult: 'recaptcha-passed',
+ }),
+ );
+ });
+
+ test('turnstile', async () => {
+ await assertFailure(
+ captchaErrorCodes.requestFailed,
+ service.save('turnstile', {
+ sitekey: 'turnstile-sitekey',
+ secret: 'turnstile-secret',
+ captchaResult: 'turnstile-passed',
+ }),
+ );
+ });
+
+ // testchapchaはrequestFailedがない
+ });
+
+ describe('verificationFailed', () => {
+ beforeEach(() => {
+ failureVerificationMock({ success: false, valid: false, 'error-codes': ['code01', 'code02'] });
+ });
+
+ test('hcaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.verificationFailed,
+ service.save('hcaptcha', {
+ sitekey: 'hcaptcha-sitekey',
+ secret: 'hcaptcha-secret',
+ captchaResult: 'hccaptcha-passed',
+ }),
+ );
+ });
+
+ test('mcaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.verificationFailed,
+ service.save('mcaptcha', {
+ sitekey: 'mcaptcha-sitekey',
+ secret: 'mcaptcha-secret',
+ instanceUrl: host,
+ captchaResult: 'mcaptcha-passed',
+ }),
+ );
+ });
+
+ test('recaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.verificationFailed,
+ service.save('recaptcha', {
+ sitekey: 'recaptcha-sitekey',
+ secret: 'recaptcha-secret',
+ captchaResult: 'recaptcha-passed',
+ }),
+ );
+ });
+
+ test('turnstile', async () => {
+ await assertFailure(
+ captchaErrorCodes.verificationFailed,
+ service.save('turnstile', {
+ sitekey: 'turnstile-sitekey',
+ secret: 'turnstile-secret',
+ captchaResult: 'turnstile-passed',
+ }),
+ );
+ });
+
+ test('testcaptcha', async () => {
+ await assertFailure(
+ captchaErrorCodes.verificationFailed,
+ service.save('testcaptcha', {
+ captchaResult: 'testcaptcha-failed',
+ }),
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/packages/backend/test/unit/CustomEmojiService.ts b/packages/backend/test/unit/CustomEmojiService.ts
new file mode 100644
index 0000000000..8c3dac69e8
--- /dev/null
+++ b/packages/backend/test/unit/CustomEmojiService.ts
@@ -0,0 +1,823 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { afterEach, beforeAll, describe, test } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { IdService } from '@/core/IdService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { UtilityService } from '@/core/UtilityService.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { EmojisRepository } from '@/models/_.js';
+import { MiEmoji } from '@/models/Emoji.js';
+import { CoreModule } from '@/core/CoreModule.js';
+import { DriveService } from '@/core//DriveService.js';
+import { DataSource } from 'typeorm';
+
+describe('CustomEmojiService', () => {
+ let app: TestingModule;
+ let service: CustomEmojiService;
+
+ let emojisRepository: EmojisRepository;
+ let idService: IdService;
+
+ beforeAll(async () => {
+ app = await Test
+ .createTestingModule({
+ imports: [
+ GlobalModule,
+ CoreModule,
+ ],
+ providers: [
+ CustomEmojiService,
+ UtilityService,
+ IdService,
+ EmojiEntityService,
+ ModerationLogService,
+ GlobalEventService,
+ DriveService,
+ ],
+ })
+ .compile();
+ app.enableShutdownHooks();
+
+ service = app.get<CustomEmojiService>(CustomEmojiService);
+ emojisRepository = app.get<EmojisRepository>(DI.emojisRepository);
+ idService = app.get<IdService>(IdService);
+ await app.get<DataSource>(DI.db).query("set session time zone 'UTC'");
+ });
+
+ describe('fetchEmojis', () => {
+ async function insert(data: Partial<MiEmoji>[]) {
+ for (const d of data) {
+ const id = idService.gen();
+ await emojisRepository.insert({
+ id: id,
+ updatedAt: new Date(),
+ ...d,
+ });
+ }
+ }
+
+ function call(params: Parameters<CustomEmojiService['fetchEmojis']>['0']) {
+ return service.fetchEmojis(
+ params,
+ {
+ // テスト向けに
+ sortKeys: ['+id'],
+ },
+ );
+ }
+
+ function defaultData(suffix: string, override?: Partial<MiEmoji>): Partial<MiEmoji> {
+ return {
+ name: `emoji${suffix}`,
+ host: null,
+ category: 'default',
+ originalUrl: `https://example.com/emoji${suffix}.png`,
+ publicUrl: `https://example.com/emoji${suffix}.png`,
+ type: 'image/png',
+ aliases: [`emoji${suffix}`],
+ license: 'CC0',
+ isSensitive: false,
+ localOnly: false,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: [],
+ ...override,
+ };
+ }
+
+ afterEach(async () => {
+ await emojisRepository.delete({});
+ });
+
+ describe('単独', () => {
+ test('updatedAtFrom', async () => {
+ await insert([
+ defaultData('001', { updatedAt: new Date('2021-01-01T00:00:00.000Z') }),
+ defaultData('002', { updatedAt: new Date('2021-01-02T00:00:00.000Z') }),
+ defaultData('003', { updatedAt: new Date('2021-01-03T00:00:00.000Z') }),
+ ]);
+
+ const actual = await call({
+ query: {
+ updatedAtFrom: '2021-01-02T00:00:00.000Z',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ expect(actual.emojis[1].name).toBe('emoji003');
+ });
+
+ test('updatedAtTo', async () => {
+ await insert([
+ defaultData('001', { updatedAt: new Date('2021-01-01T00:00:00.000Z') }),
+ defaultData('002', { updatedAt: new Date('2021-01-02T00:00:00.000Z') }),
+ defaultData('003', { updatedAt: new Date('2021-01-03T00:00:00.000Z') }),
+ ]);
+
+ const actual = await call({
+ query: {
+ updatedAtTo: '2021-01-02T00:00:00.000Z',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji002');
+ });
+
+ describe('name', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001'),
+ defaultData('002'),
+ ]);
+
+ const actual = await call({
+ query: {
+ name: 'emoji001',
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001'),
+ defaultData('002'),
+ ]);
+
+ const actual = await call({
+ query: {
+ name: 'emoji001 emoji002',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji002');
+ });
+
+ test('keyword', async () => {
+ await insert([
+ defaultData('001'),
+ defaultData('002'),
+ defaultData('003', { name: 'em003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ name: 'oji',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji002');
+ });
+
+ test('escape', async () => {
+ await insert([
+ defaultData('001'),
+ ]);
+
+ const actual = await call({
+ query: {
+ name: '%',
+ },
+ });
+
+ expect(actual.allCount).toBe(0);
+ });
+ });
+
+ describe('host', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001', { host: 'example.com' }),
+ defaultData('002', { host: 'example.com' }),
+ defaultData('003', { host: '1.example.com' }),
+ defaultData('004', { host: '2.example.com' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ host: 'example.com',
+ hostType: 'remote',
+ },
+ });
+
+ expect(actual.allCount).toBe(4);
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001', { host: 'example.com' }),
+ defaultData('002', { host: 'example.com' }),
+ defaultData('003', { host: '1.example.com' }),
+ defaultData('004', { host: '2.example.com' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ host: '1.example.com 2.example.com',
+ hostType: 'remote',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji003');
+ expect(actual.emojis[1].name).toBe('emoji004');
+ });
+
+ test('keyword', async () => {
+ await insert([
+ defaultData('001', { host: 'example.com' }),
+ defaultData('002', { host: 'example.com' }),
+ defaultData('003', { host: '1.example.com' }),
+ defaultData('004', { host: '2.example.com' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ host: 'example',
+ hostType: 'remote',
+ },
+ });
+
+ expect(actual.allCount).toBe(4);
+ });
+
+ test('escape', async () => {
+ await insert([
+ defaultData('001', { host: 'example.com' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ host: '%',
+ hostType: 'remote',
+ },
+ });
+
+ expect(actual.allCount).toBe(0);
+ });
+ });
+
+ describe('uri', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001', { uri: 'uri001' }),
+ defaultData('002', { uri: 'uri002' }),
+ defaultData('003', { uri: 'uri003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ uri: 'uri002',
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001', { uri: 'uri001' }),
+ defaultData('002', { uri: 'uri002' }),
+ defaultData('003', { uri: 'uri003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ uri: 'uri001 uri003',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji003');
+ });
+
+ test('keyword', async () => {
+ await insert([
+ defaultData('001', { uri: 'uri001' }),
+ defaultData('002', { uri: 'uri002' }),
+ defaultData('003', { uri: 'uri003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ uri: 'ri',
+ },
+ });
+
+ expect(actual.allCount).toBe(3);
+ });
+
+ test('escape', async () => {
+ await insert([
+ defaultData('001', { uri: 'uri001' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ uri: '%',
+ },
+ });
+
+ expect(actual.allCount).toBe(0);
+ });
+ });
+
+ describe('publicUrl', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001', { publicUrl: 'publicUrl001' }),
+ defaultData('002', { publicUrl: 'publicUrl002' }),
+ defaultData('003', { publicUrl: 'publicUrl003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ publicUrl: 'publicUrl002',
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001', { publicUrl: 'publicUrl001' }),
+ defaultData('002', { publicUrl: 'publicUrl002' }),
+ defaultData('003', { publicUrl: 'publicUrl003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ publicUrl: 'publicUrl001 publicUrl003',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji003');
+ });
+
+ test('keyword', async () => {
+ await insert([
+ defaultData('001', { publicUrl: 'publicUrl001' }),
+ defaultData('002', { publicUrl: 'publicUrl002' }),
+ defaultData('003', { publicUrl: 'publicUrl003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ publicUrl: 'Url',
+ },
+ });
+
+ expect(actual.allCount).toBe(3);
+ });
+
+ test('escape', async () => {
+ await insert([
+ defaultData('001', { publicUrl: 'publicUrl001' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ publicUrl: '%',
+ },
+ });
+
+ expect(actual.allCount).toBe(0);
+ });
+ });
+
+ describe('type', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001', { type: 'type001' }),
+ defaultData('002', { type: 'type002' }),
+ defaultData('003', { type: 'type003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ type: 'type002',
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001', { type: 'type001' }),
+ defaultData('002', { type: 'type002' }),
+ defaultData('003', { type: 'type003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ type: 'type001 type003',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji003');
+ });
+
+ test('keyword', async () => {
+ await insert([
+ defaultData('001', { type: 'type001' }),
+ defaultData('002', { type: 'type002' }),
+ defaultData('003', { type: 'type003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ type: 'pe',
+ },
+ });
+
+ expect(actual.allCount).toBe(3);
+ });
+
+ test('escape', async () => {
+ await insert([
+ defaultData('001', { type: 'type001' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ type: '%',
+ },
+ });
+
+ expect(actual.allCount).toBe(0);
+ });
+ });
+
+ describe('aliases', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001', { aliases: ['alias001', 'alias002'] }),
+ defaultData('002', { aliases: ['alias002'] }),
+ defaultData('003', { aliases: ['alias003'] }),
+ ]);
+
+ const actual = await call({
+ query: {
+ aliases: 'alias002',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji002');
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001', { aliases: ['alias001', 'alias002'] }),
+ defaultData('002', { aliases: ['alias002', 'alias004'] }),
+ defaultData('003', { aliases: ['alias003'] }),
+ defaultData('004', { aliases: ['alias004'] }),
+ ]);
+
+ const actual = await call({
+ query: {
+ aliases: 'alias001 alias004',
+ },
+ });
+
+ expect(actual.allCount).toBe(3);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji002');
+ expect(actual.emojis[2].name).toBe('emoji004');
+ });
+
+ test('keyword', async () => {
+ await insert([
+ defaultData('001', { aliases: ['alias001', 'alias002'] }),
+ defaultData('002', { aliases: ['alias002', 'alias004'] }),
+ defaultData('003', { aliases: ['alias003'] }),
+ defaultData('004', { aliases: ['alias004'] }),
+ ]);
+
+ const actual = await call({
+ query: {
+ aliases: 'ias',
+ },
+ });
+
+ expect(actual.allCount).toBe(4);
+ });
+
+ test('escape', async () => {
+ await insert([
+ defaultData('001', { aliases: ['alias001', 'alias002'] }),
+ ]);
+
+ const actual = await call({
+ query: {
+ aliases: '%',
+ },
+ });
+
+ expect(actual.allCount).toBe(0);
+ });
+ });
+
+ describe('category', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001', { category: 'category001' }),
+ defaultData('002', { category: 'category002' }),
+ defaultData('003', { category: 'category003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ category: 'category002',
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001', { category: 'category001' }),
+ defaultData('002', { category: 'category002' }),
+ defaultData('003', { category: 'category003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ category: 'category001 category003',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji003');
+ });
+
+ test('keyword', async () => {
+ await insert([
+ defaultData('001', { category: 'category001' }),
+ defaultData('002', { category: 'category002' }),
+ defaultData('003', { category: 'category003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ category: 'egory',
+ },
+ });
+
+ expect(actual.allCount).toBe(3);
+ });
+
+ test('escape', async () => {
+ await insert([
+ defaultData('001', { category: 'category001' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ category: '%',
+ },
+ });
+
+ expect(actual.allCount).toBe(0);
+ });
+ });
+
+ describe('license', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001', { license: 'license001' }),
+ defaultData('002', { license: 'license002' }),
+ defaultData('003', { license: 'license003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ license: 'license002',
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001', { license: 'license001' }),
+ defaultData('002', { license: 'license002' }),
+ defaultData('003', { license: 'license003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ license: 'license001 license003',
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji003');
+ });
+
+ test('keyword', async () => {
+ await insert([
+ defaultData('001', { license: 'license001' }),
+ defaultData('002', { license: 'license002' }),
+ defaultData('003', { license: 'license003' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ license: 'cense',
+ },
+ });
+
+ expect(actual.allCount).toBe(3);
+ });
+
+ test('escape', async () => {
+ await insert([
+ defaultData('001', { license: 'license001' }),
+ ]);
+
+ const actual = await call({
+ query: {
+ license: '%',
+ },
+ });
+
+ expect(actual.allCount).toBe(0);
+ });
+ });
+
+ describe('isSensitive', () => {
+ test('true', async () => {
+ await insert([
+ defaultData('001', { isSensitive: true }),
+ defaultData('002', { isSensitive: false }),
+ defaultData('003', { isSensitive: true }),
+ ]);
+
+ const actual = await call({
+ query: {
+ isSensitive: true,
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji003');
+ });
+
+ test('false', async () => {
+ await insert([
+ defaultData('001', { isSensitive: true }),
+ defaultData('002', { isSensitive: false }),
+ defaultData('003', { isSensitive: true }),
+ ]);
+
+ const actual = await call({
+ query: {
+ isSensitive: false,
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ });
+
+ test('null', async () => {
+ await insert([
+ defaultData('001', { isSensitive: true }),
+ defaultData('002', { isSensitive: false }),
+ defaultData('003', { isSensitive: true }),
+ ]);
+
+ const actual = await call({
+ query: {},
+ });
+
+ expect(actual.allCount).toBe(3);
+ });
+ });
+
+ describe('localOnly', () => {
+ test('true', async () => {
+ await insert([
+ defaultData('001', { localOnly: true }),
+ defaultData('002', { localOnly: false }),
+ defaultData('003', { localOnly: true }),
+ ]);
+
+ const actual = await call({
+ query: {
+ localOnly: true,
+ },
+ });
+
+ expect(actual.allCount).toBe(2);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji003');
+ });
+
+ test('false', async () => {
+ await insert([
+ defaultData('001', { localOnly: true }),
+ defaultData('002', { localOnly: false }),
+ defaultData('003', { localOnly: true }),
+ ]);
+
+ const actual = await call({
+ query: {
+ localOnly: false,
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ });
+
+ test('null', async () => {
+ await insert([
+ defaultData('001', { localOnly: true }),
+ defaultData('002', { localOnly: false }),
+ defaultData('003', { localOnly: true }),
+ ]);
+
+ const actual = await call({
+ query: {},
+ });
+
+ expect(actual.allCount).toBe(3);
+ });
+ });
+
+ describe('roleId', () => {
+ test('single', async () => {
+ await insert([
+ defaultData('001', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role001'] }),
+ defaultData('002', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role002'] }),
+ defaultData('003', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role003'] }),
+ ]);
+
+ const actual = await call({
+ query: {
+ roleIds: ['role002'],
+ },
+ });
+
+ expect(actual.allCount).toBe(1);
+ expect(actual.emojis[0].name).toBe('emoji002');
+ });
+
+ test('multi', async () => {
+ await insert([
+ defaultData('001', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role001'] }),
+ defaultData('002', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role002', 'role003'] }),
+ defaultData('003', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role003'] }),
+ defaultData('004', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role004'] }),
+ ]);
+
+ const actual = await call({
+ query: {
+ roleIds: ['role001', 'role003'],
+ },
+ });
+
+ expect(actual.allCount).toBe(3);
+ expect(actual.emojis[0].name).toBe('emoji001');
+ expect(actual.emojis[1].name).toBe('emoji002');
+ expect(actual.emojis[2].name).toBe('emoji003');
+ });
+ });
+ });
+ });
+});
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index 8d5683329f..e54c006a4f 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -45,6 +45,50 @@ describe('MfmService', () => {
const output = '<p><pre><code>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
+
+ test('ruby', () => {
+ const input = '$[ruby some text ignore me]';
+ const output = '<p><ruby>some<rp>(</rp><rt>text</rt><rp>)</rp></ruby></p>';
+ assert.equal(mfmService.toHtml(mfm.parse(input)), output);
+ });
+
+ test('ruby2', () => {
+ const input = '$[ruby *some text* ignore me]';
+ const output = '<p><ruby><i>some text</i><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
+ assert.equal(mfmService.toHtml(mfm.parse(input)), output);
+ });
+
+ test('ruby 3', () => {
+ const input = '$[ruby $[group *some* text] ignore me]';
+ const output = '<p><ruby><span><i>some</i> text</span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
+ assert.equal(mfmService.toHtml(mfm.parse(input)), output);
+ });
+ });
+
+ describe('toMastoApiHtml', () => {
+ test('br', async () => {
+ const input = 'foo\nbar\nbaz';
+ const output = '<p><span>foo<br>bar<br>baz</span></p>';
+ assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
+ });
+
+ test('br alt', async () => {
+ const input = 'foo\r\nbar\rbaz';
+ const output = '<p><span>foo<br>bar<br>baz</span></p>';
+ assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
+ });
+
+ test('escape', async () => {
+ const input = '```\n<p>Hello, world!</p>\n```';
+ const output = '<p><pre><code>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre></p>';
+ assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
+ });
+
+ test('ruby', async () => {
+ const input = '$[ruby $[group *some* text] ignore me]';
+ const output = '<p><ruby><span><span>*some*</span><span> text</span></span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
+ assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
+ });
});
describe('fromHtml', () => {
@@ -108,6 +152,24 @@ describe('MfmService', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <a></a> d</p>'), 'a d');
});
+ test('ruby', () => {
+ assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'), 'a $[ruby Misskey ミスキー] b');
+ assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'), 'a $[ruby Misskey ミスキー]$[ruby Misskey ミスキー] b');
+ });
+
+ test('ruby with spaces', () => {
+ assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Miss key<rp>(</rp><rt>ミスキー</rt><rp>)</rp> b</ruby> c</p>'), 'a $[ruby $[group Miss key] ミスキー] b c');
+ assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp> b</ruby> c</p>'), 'a $[ruby $[group Misskey] ミス キー] b c');
+ assert.deepStrictEqual(
+ mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'),
+ 'a $[ruby Misskey ミスキー]$[ruby $[group Misskey] ミス キー]$[ruby Misskey ミスキー] b'
+ );
+ });
+
+ test('ruby with other inline tags', () => {
+ assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby><strong>Misskey</strong><rp>(</rp><rt>ミスキー</rt><rp>)</rp> b</ruby> c</p>'), 'a $[ruby **Misskey** ミスキー] b c');
+ });
+
test('mention', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d');
});
@@ -115,5 +177,12 @@ describe('MfmService', () => {
test('hashtag', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <a href="https://example.com/tags/a">#a</a> d</p>', ['#a']), 'a #a d');
});
+
+ test('ruby', () => {
+ assert.deepStrictEqual(
+ mfmService.fromHtml('<ruby> <i>some</i> text <rp>(</rp><rt>ignore me</rt><rp>)</rp> and <rt>more</rt></ruby>'),
+ '$[ruby $[group <i>some</i> text ] ignore me]$[ruby $[group and ] more]'
+ );
+ });
});
});
diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts
index ccf9ed1e6d..f4ecfef34d 100644
--- a/packages/backend/test/unit/NoteCreateService.ts
+++ b/packages/backend/test/unit/NoteCreateService.ts
@@ -61,6 +61,7 @@ describe('NoteCreateService', () => {
replyUserHost: null,
renoteUserId: null,
renoteUserHost: null,
+ processErrors: [],
};
const poll: IPoll = {
diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
index 7df991c15c..efed905e02 100644
--- a/packages/backend/test/unit/SigninWithPasskeyApiService.ts
+++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
@@ -20,7 +20,7 @@ import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiSe
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { SigninService } from '@/server/api/SigninService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { LimitInfo } from '@/misc/rate-limit-utils.js';
const moduleMocker = new ModuleMocker(global);
diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts
index 5401dd74d8..fee4acb305 100644
--- a/packages/backend/test/unit/SystemWebhookService.ts
+++ b/packages/backend/test/unit/SystemWebhookService.ts
@@ -314,9 +314,10 @@ describe('SystemWebhookService', () => {
isActive: true,
on: ['abuseReport'],
});
- await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
+ await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any);
- expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
+ expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1);
+ expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook);
});
test('非アクティブなWebhookはキューに追加されない', async () => {
@@ -324,7 +325,7 @@ describe('SystemWebhookService', () => {
isActive: false,
on: ['abuseReport'],
});
- await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
+ await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
@@ -338,11 +339,49 @@ describe('SystemWebhookService', () => {
isActive: true,
on: ['abuseReportResolved'],
});
- await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
- await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
+ await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
+
+ test('混在した時、有効かつ許可されたイベント種別のみ', async () => {
+ const webhook1 = await createWebhook({
+ isActive: true,
+ on: ['abuseReport'],
+ });
+ const webhook2 = await createWebhook({
+ isActive: true,
+ on: ['abuseReportResolved'],
+ });
+ const webhook3 = await createWebhook({
+ isActive: false,
+ on: ['abuseReport'],
+ });
+ const webhook4 = await createWebhook({
+ isActive: false,
+ on: ['abuseReportResolved'],
+ });
+ await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any);
+
+ expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1);
+ expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook1);
+ });
+
+ test('除外指定した場合は送信されない', async () => {
+ const webhook1 = await createWebhook({
+ isActive: true,
+ on: ['abuseReport'],
+ });
+ const webhook2 = await createWebhook({
+ isActive: true,
+ on: ['abuseReport'],
+ });
+
+ await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any, { excludes: [webhook2.id] });
+
+ expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1);
+ expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook1);
+ });
});
describe('fetchActiveSystemWebhooks', () => {
diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts
index 0e88835a02..db8f96df28 100644
--- a/packages/backend/test/unit/UserWebhookService.ts
+++ b/packages/backend/test/unit/UserWebhookService.ts
@@ -1,4 +1,3 @@
-
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
@@ -71,7 +70,7 @@ describe('UserWebhookService', () => {
LoggerService,
GlobalEventService,
{
- provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
+ provide: QueueService, useFactory: () => ({ userWebhookDeliver: jest.fn() }),
},
],
})
@@ -242,4 +241,92 @@ describe('UserWebhookService', () => {
});
});
});
+
+ describe('アプリを毎回作り直す必要があるグループ', () => {
+ beforeEach(async () => {
+ await beforeAllImpl();
+ await beforeEachImpl();
+ });
+
+ afterEach(async () => {
+ await afterEachImpl();
+ await afterAllImpl();
+ });
+
+ describe('enqueueUserWebhook', () => {
+ test('キューに追加成功', async () => {
+ const webhook = await createWebhook({
+ active: true,
+ on: ['note'],
+ });
+ await service.enqueueUserWebhook(webhook.userId, 'note', { foo: 'bar' } as any);
+
+ expect(queueService.userWebhookDeliver).toHaveBeenCalledTimes(1);
+ expect(queueService.userWebhookDeliver.mock.calls[0][0] as MiWebhook).toEqual(webhook);
+ });
+
+ test('非アクティブなWebhookはキューに追加されない', async () => {
+ const webhook = await createWebhook({
+ active: false,
+ on: ['note'],
+ });
+ await service.enqueueUserWebhook(webhook.userId, 'note', { foo: 'bar' } as any);
+
+ expect(queueService.userWebhookDeliver).not.toHaveBeenCalled();
+ });
+
+ test('未許可のイベント種別が渡された場合はWebhookはキューに追加されない', async () => {
+ const webhook1 = await createWebhook({
+ active: true,
+ on: [],
+ });
+ const webhook2 = await createWebhook({
+ active: true,
+ on: ['note'],
+ });
+ await service.enqueueUserWebhook(webhook1.userId, 'renote', { foo: 'bar' } as any);
+ await service.enqueueUserWebhook(webhook2.userId, 'renote', { foo: 'bar' } as any);
+
+ expect(queueService.userWebhookDeliver).not.toHaveBeenCalled();
+ });
+
+ test('ユーザIDが異なるWebhookはキューに追加されない', async () => {
+ const webhook = await createWebhook({
+ active: true,
+ on: ['note'],
+ });
+ await service.enqueueUserWebhook(idService.gen(), 'note', { foo: 'bar' } as any);
+
+ expect(queueService.userWebhookDeliver).not.toHaveBeenCalled();
+ });
+
+ test('混在した時、有効かつ許可されたイベント種別のみ', async () => {
+ const userId = root.id;
+ const webhook1 = await createWebhook({
+ userId,
+ active: true,
+ on: ['note'],
+ });
+ const webhook2 = await createWebhook({
+ userId,
+ active: true,
+ on: ['renote'],
+ });
+ const webhook3 = await createWebhook({
+ userId,
+ active: false,
+ on: ['note'],
+ });
+ const webhook4 = await createWebhook({
+ userId,
+ active: false,
+ on: ['renote'],
+ });
+ await service.enqueueUserWebhook(userId, 'note', { foo: 'bar' } as any);
+
+ expect(queueService.userWebhookDeliver).toHaveBeenCalledTimes(1);
+ expect(queueService.userWebhookDeliver.mock.calls[0][0] as MiWebhook).toEqual(webhook1);
+ });
+ });
+ });
});
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index 73d6186edf..553467499b 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { IdService } from '@/core/IdService.js';
+
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
@@ -20,7 +22,7 @@ import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
-import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
+import { MiMeta, MiNote, MiUser, UserProfilesRepository, UserPublickeysRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DownloadService } from '@/core/DownloadService.js';
@@ -93,6 +95,8 @@ describe('ActivityPub', () => {
let rendererService: ApRendererService;
let jsonLdService: JsonLdService;
let resolver: MockResolver;
+ let idService: IdService;
+ let userPublickeysRepository: UserPublickeysRepository;
const metaInitial = {
cacheRemoteFiles: true,
@@ -140,6 +144,8 @@ describe('ActivityPub', () => {
imageService = app.get<ApImageService>(ApImageService);
jsonLdService = app.get<JsonLdService>(JsonLdService);
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
+ idService = app.get<IdService>(IdService);
+ userPublickeysRepository = app.get<UserPublickeysRepository>(DI.userPublickeysRepository);
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
const federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService);
@@ -477,4 +483,210 @@ describe('ActivityPub', () => {
});
});
});
+
+ describe(ApRendererService, () => {
+ let note: MiNote;
+ let author: MiUser;
+
+ beforeEach(() => {
+ author = new MiUser({
+ id: idService.gen(),
+ });
+ note = new MiNote({
+ id: idService.gen(),
+ userId: author.id,
+ visibility: 'public',
+ localOnly: false,
+ text: 'Note text',
+ cw: null,
+ renoteCount: 0,
+ repliesCount: 0,
+ clippedCount: 0,
+ reactions: {},
+ fileIds: [],
+ attachedFileTypes: [],
+ visibleUserIds: [],
+ mentions: [],
+ // This is fucked tbh - it's JSON stored in a TEXT column that gets parsed/serialized all over the place
+ mentionedRemoteUsers: '[]',
+ reactionAndUserPairCache: [],
+ emojis: [],
+ tags: [],
+ hasPoll: false,
+ });
+ });
+
+ describe('renderNote', () => {
+ describe('summary', () => {
+ // I actually don't know why it does this, but the logic was already there so I've preserved it.
+ it('should be zero-width space when CW is empty string', async () => {
+ note.cw = '';
+
+ const result = await rendererService.renderNote(note, author, false);
+
+ expect(result.summary).toBe(String.fromCharCode(0x200B));
+ });
+
+ it('should be undefined when CW is null', async () => {
+ const result = await rendererService.renderNote(note, author, false);
+
+ expect(result.summary).toBeUndefined();
+ });
+
+ it('should be CW when present without mandatoryCW', async () => {
+ note.cw = 'original';
+
+ const result = await rendererService.renderNote(note, author, false);
+
+ expect(result.summary).toBe('original');
+ });
+
+ it('should be mandatoryCW when present without CW', async () => {
+ author.mandatoryCW = 'mandatory';
+
+ const result = await rendererService.renderNote(note, author, false);
+
+ expect(result.summary).toBe('mandatory');
+ });
+
+ it('should be merged when CW and mandatoryCW are both present', async () => {
+ note.cw = 'original';
+ author.mandatoryCW = 'mandatory';
+
+ const result = await rendererService.renderNote(note, author, false);
+
+ expect(result.summary).toBe('original, mandatory');
+ });
+
+ it('should be CW when CW includes mandatoryCW', async () => {
+ note.cw = 'original and mandatory';
+ author.mandatoryCW = 'mandatory';
+
+ const result = await rendererService.renderNote(note, author, false);
+
+ expect(result.summary).toBe('original and mandatory');
+ });
+ });
+ });
+
+ describe('renderUpnote', () => {
+ describe('summary', () => {
+ // I actually don't know why it does this, but the logic was already there so I've preserved it.
+ it('should be zero-width space when CW is empty string', async () => {
+ note.cw = '';
+
+ const result = await rendererService.renderUpNote(note, author, false);
+
+ expect(result.summary).toBe(String.fromCharCode(0x200B));
+ });
+
+ it('should be undefined when CW is null', async () => {
+ const result = await rendererService.renderUpNote(note, author, false);
+
+ expect(result.summary).toBeUndefined();
+ });
+
+ it('should be CW when present without mandatoryCW', async () => {
+ note.cw = 'original';
+
+ const result = await rendererService.renderUpNote(note, author, false);
+
+ expect(result.summary).toBe('original');
+ });
+
+ it('should be mandatoryCW when present without CW', async () => {
+ author.mandatoryCW = 'mandatory';
+
+ const result = await rendererService.renderUpNote(note, author, false);
+
+ expect(result.summary).toBe('mandatory');
+ });
+
+ it('should be merged when CW and mandatoryCW are both present', async () => {
+ note.cw = 'original';
+ author.mandatoryCW = 'mandatory';
+
+ const result = await rendererService.renderUpNote(note, author, false);
+
+ expect(result.summary).toBe('original, mandatory');
+ });
+
+ it('should be CW when CW includes mandatoryCW', async () => {
+ note.cw = 'original and mandatory';
+ author.mandatoryCW = 'mandatory';
+
+ const result = await rendererService.renderUpNote(note, author, false);
+
+ expect(result.summary).toBe('original and mandatory');
+ });
+ });
+ });
+ });
+
+ describe(ApPersonService, () => {
+ describe('createPerson', () => {
+ it('should trim publicKey', async () => {
+ const actor = createRandomActor();
+ actor.publicKey = {
+ id: `${actor.id}#main-key`,
+ publicKeyPem: ' key material\t\n\r\n \n',
+ };
+ resolver.register(actor.id, actor);
+
+ const user = await personService.createPerson(actor.id, resolver);
+ const publicKey = await userPublickeysRepository.findOneBy({ userId: user.id });
+
+ expect(publicKey).not.toBeNull();
+ expect(publicKey?.keyPem).toBe('key material');
+ });
+
+ it('should accept SocialHome actor', async () => {
+ // This is taken from a real SocialHome actor, including the 13,905 newline characters in the public key.
+ const actor = {
+ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', {
+ 'pyfed': 'https://docs.jasonrobinson.me/ns/python-federation#',
+ 'diaspora': 'https://diasporafoundation.org/ns/',
+ 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
+ }],
+ id: 'https://socialhome.network/u/hq/',
+ type: 'Person',
+ inbox: 'https://socialhome.network/u/hq/inbox/',
+ 'diaspora:guid': '7538bd1b-d3a8-49a5-bf00-db63fcc9114f',
+ 'diaspora:handle': 'hq@socialhome.network',
+ publicKey: {
+ id: 'https://socialhome.network/u/hq/#main-key',
+ owner: 'https://socialhome.network/u/hq/',
+ publicKeyPem: '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAg39sDmTAJ7l9bl5jYLmj\nKYnDZJgRiO/WR+V1HEMEsRoEPTxJzWe+Ou7YTUhOOvDRu5ncEn3ictF3/BxhhQC1\nQwUKYlfuU1R7PyGqWtGm6300mDAmbq+eyC+fwV9FbkCm9npRatZfnZXZWuCgA6f7\nWmmBw09QVZQ6Ypu+7CF/Q6bv0E5B2hieTSbRgavdSkEopMyJhPs5/X6Hh4XYSi7t\nYEg9vD0d0J9QJSnCTYIZT145cV1DANV/4KjhKkYgvt4hLNOKZ1v4QC57K+PFna9N\ntxm1nMxwjpBPus8LQeDii/MwKoiZ7LBjeflm0C9AMFlNPB9iq3rEXo3eyCEb7Lyr\nEp+oqYNfopFIRPNfhBxtkx5ioUXty3cx1WnZtehqGdpOcb1wUatW5IjV8tlfLIr7\nrDNCxgGnScR6h7++BHYDdDVBgGUkC5ELIxxSMqlYMiBGVmYdIoAGO6nuqw4bp5l3\nUf07d28GoZgcRBVZWC/xOtRb7E6PTzsE7xd51UijusRC79lnapzTWY9GAY0ZYu+w\nbAxO7u3+Knr6EXZkGkmrElKIT2N6SPJY3Xo91+PT1Y77JMFkkWlEX9IO08fALsqg\nbMSKNQ8WfyHCTjaiH3n4BdgTjP4kRm2OhczxvgCFvtcOK+M60YdwM6MOZDEOVtGU\nGIYA1mtQW7a8jb5QPTQu9GcCAwEAAQ==\n-----END PUBLIC KEY-----' + ''.padEnd(13905, '\n'),
+ },
+ endpoints: { 'sharedInbox': 'https://socialhome.network/receive/public/' },
+ followers: 'https://socialhome.network/u/hq/followers/',
+ following: 'https://socialhome.network/u/hq/following/',
+ icon: {
+ type: 'Image',
+ 'pyfed:inlineImage': false,
+ mediaType: 'image/png',
+ url: 'https://socialhome.network/media/__sized__/profiles/Socialhome-dark-600-crop-c0-5__0-5-300x300.png',
+ },
+ manuallyApprovesFollowers: false,
+ name: 'Socialhome HQ',
+ outbox: 'https://socialhome.network/u/hq/outbox/',
+ preferredUsername: 'hq',
+ published: '2017-01-29T19:28:19+00:00',
+ updated: '2025-02-17T23:11:30+00:00',
+ url: 'https://socialhome.network/p/7538bd1b-d3a8-49a5-bf00-db63fcc9114f/',
+ };
+ resolver.register(actor.id, actor);
+ resolver.register(actor.publicKey.id, actor.publicKey);
+ resolver.register(actor.followers, { id: actor.followers, type: 'Collection', totalItems: 0, items: [] } satisfies ICollection);
+ resolver.register(actor.following, { id: actor.following, type: 'Collection', totalItems: 0, items: [] } satisfies ICollection);
+ resolver.register(actor.outbox, { id: actor.outbox, type: 'Collection', totalItems: 0, items: [] } satisfies ICollection);
+
+ const user = await personService.createPerson(actor.id, resolver);
+ const publicKey = await userPublickeysRepository.findOneBy({ userId: user.id });
+
+ expect(user.uri).toBe(actor.id);
+ expect(publicKey).not.toBeNull();
+ });
+ });
+ });
});
diff --git a/packages/backend/test/unit/core/activitypub/ApUtilityService.ts b/packages/backend/test/unit/core/activitypub/ApUtilityService.ts
new file mode 100644
index 0000000000..325a94dc5a
--- /dev/null
+++ b/packages/backend/test/unit/core/activitypub/ApUtilityService.ts
@@ -0,0 +1,354 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { UtilityService } from '@/core/UtilityService.js';
+import type { IObject } from '@/core/activitypub/type.js';
+import type { EnvService } from '@/core/EnvService.js';
+import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
+
+describe(ApUtilityService, () => {
+ let serviceUnderTest: ApUtilityService;
+ let env: Record<string, string>;
+
+ beforeEach(() => {
+ const utilityService = {
+ punyHostPSLDomain(input: string) {
+ const host = new URL(input).host;
+ const parts = host.split('.');
+ return `${parts[parts.length - 2]}.${parts[parts.length - 1]}`;
+ },
+ } as unknown as UtilityService;
+
+ env = {};
+ const envService = {
+ env,
+ } as unknown as EnvService;
+
+ serviceUnderTest = new ApUtilityService(utilityService, envService);
+ });
+
+ describe('assertIdMatchesUrlAuthority', () => {
+ it('should return when input matches', () => {
+ const object = { id: 'https://first.example.com' } as IObject;
+ const url = 'https://second.example.com';
+
+ expect(() => {
+ serviceUnderTest.assertIdMatchesUrlAuthority(object, url);
+ }).not.toThrow();
+ });
+
+ it('should throw when id is missing', () => {
+ const object = { id: undefined } as IObject;
+ const url = 'https://second.example.com';
+
+ expect(() => {
+ serviceUnderTest.assertIdMatchesUrlAuthority(object, url);
+ }).toThrow();
+ });
+
+ it('should throw when id does not match', () => {
+ const object = { id: 'https://other-domain.com' } as IObject;
+ const url = 'https://second.example.com';
+
+ expect(() => {
+ serviceUnderTest.assertIdMatchesUrlAuthority(object, url);
+ }).toThrow();
+ });
+ });
+
+ describe('haveSameAuthority', () => {
+ it('should return true when URLs match', () => {
+ const url = 'https://example.com';
+
+ const result = serviceUnderTest.haveSameAuthority(url, url);
+
+ expect(result).toBeTruthy();
+ });
+
+ it('should return true when URLs have same host', () => {
+ const first = 'https://example.com/first';
+ const second = 'https://example.com/second';
+
+ const result = serviceUnderTest.haveSameAuthority(first, second);
+
+ expect(result).toBeTruthy();
+ });
+
+ it('should return true when URLs have same authority', () => {
+ const first = 'https://first.example.com/first';
+ const second = 'https://second.example.com/second';
+
+ const result = serviceUnderTest.haveSameAuthority(first, second);
+
+ expect(result).toBeTruthy();
+ });
+
+ it('should return false when URLs have different authority', () => {
+ const first = 'https://first.com';
+ const second = 'https://second.com';
+
+ const result = serviceUnderTest.haveSameAuthority(first, second);
+
+ expect(result).toBeFalsy();
+ });
+ });
+
+ describe('findBestObjectUrl', () => {
+ it('should return null when input is undefined', () => {
+ const object = {
+ id: 'https://example.com',
+ url: undefined,
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBeNull();
+ });
+
+ it('should return null when input is empty array', () => {
+ const object = {
+ id: 'https://example.com',
+ url: [] as string[],
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBeNull();
+ });
+
+ it('should return return url if string input matches', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: 'https://example.com/2',
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should return return url if object input matches', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: {
+ href: 'https://example.com/2',
+ } as IObject,
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should return return url if string[] input matches', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: ['https://example.com/2'],
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should return return url if object[] input matches', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: [{
+ href: 'https://example.com/2',
+ } as IObject],
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should skip invalid entries', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: [{} as IObject, 'https://example.com/2'],
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should allow empty mediaType', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: {
+ href: 'https://example.com/2',
+ } as IObject,
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should allow text/html mediaType', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: {
+ href: 'https://example.com/2',
+ mediaType: 'text/html',
+ } as IObject,
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should allow other text/ mediaTypes', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: {
+ href: 'https://example.com/2',
+ mediaType: 'text/imaginary',
+ } as IObject,
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should allow application/ld+json mediaType', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: {
+ href: 'https://example.com/2',
+ mediaType: 'application/ld+json;profile=https://www.w3.org/ns/activitystreams',
+ } as IObject,
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should allow application/activity+json mediaType', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: {
+ href: 'https://example.com/2',
+ mediaType: 'application/activity+json',
+ } as IObject,
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should reject other mediaTypes', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: [
+ {
+ href: 'https://example.com/2',
+ mediaType: 'application/json',
+ } as IObject,
+ {
+ href: 'https://example.com/3',
+ mediaType: 'image/jpeg',
+ } as IObject,
+ ],
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBeNull();
+ });
+
+ it('should return best match', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: [
+ 'https://example.com/2',
+ {
+ href: 'https://example.com/3',
+ } as IObject,
+ {
+ href: 'https://example.com/4',
+ mediaType: 'text/html',
+ } as IObject,
+ {
+ href: 'https://example.com/5',
+ mediaType: 'text/plain',
+ } as IObject,
+ {
+ href: 'https://example.com/6',
+ mediaType: 'application/ld+json',
+ } as IObject,
+ {
+ href: 'https://example.com/7',
+ mediaType: 'application/activity+json',
+ } as IObject,
+ {
+ href: 'https://example.com/8',
+ mediaType: 'image/jpeg',
+ } as IObject,
+ ],
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/4');
+ });
+
+ it('should return first match in case of ties', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: ['https://example.com/2', 'https://example.com/3'],
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should skip invalid scheme', () => {
+ const object = {
+ id: 'https://example.com/1',
+ url: ['file://example.com/1', 'https://example.com/2'],
+ } as IObject;
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should skip HTTP in production', () => {
+ // noinspection HttpUrlsUsage
+ const object = {
+ id: 'https://example.com/1',
+ url: ['http://example.com/1', 'https://example.com/2'],
+ } as IObject;
+ env.NODE_ENV = 'production';
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ expect(result).toBe('https://example.com/2');
+ });
+
+ it('should allow HTTP in non-prod', () => {
+ // noinspection HttpUrlsUsage
+ const object = {
+ id: 'https://example.com/1',
+ url: ['http://example.com/1', 'https://example.com/2'],
+ } as IObject;
+ env.NODE_ENV = 'test';
+
+ const result = serviceUnderTest.findBestObjectUrl(object);
+
+ // noinspection HttpUrlsUsage
+ expect(result).toBe('http://example.com/1');
+ });
+ });
+});
diff --git a/packages/backend/test/unit/misc/append-content-warning.ts b/packages/backend/test/unit/misc/append-content-warning.ts
new file mode 100644
index 0000000000..d25d7c4925
--- /dev/null
+++ b/packages/backend/test/unit/misc/append-content-warning.ts
@@ -0,0 +1,92 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { appendContentWarning } from '@/misc/append-content-warning.js';
+
+describe(appendContentWarning, () => {
+ it('should return additional when original is null', () => {
+ const result = appendContentWarning(null, 'additional');
+
+ expect(result).toBe('additional');
+ });
+
+ it('should return additional when original is undefined', () => {
+ const result = appendContentWarning(undefined, 'additional');
+
+ expect(result).toBe('additional');
+ });
+
+ it('should return additional when original is empty', () => {
+ const result = appendContentWarning('', 'additional');
+
+ expect(result).toBe('additional');
+ });
+
+ it('should return original when additional is empty', () => {
+ const result = appendContentWarning('original', '');
+
+ expect(result).toBe('original');
+ });
+
+ it('should append additional when it does not exist in original', () => {
+ const result = appendContentWarning('original', 'additional');
+
+ expect(result).toBe('original, additional');
+ });
+
+ it('should append additional when it exists in original but has preceeding word', () => {
+ const result = appendContentWarning('notadditional', 'additional');
+
+ expect(result).toBe('notadditional, additional');
+ });
+
+ it('should append additional when it exists in original but has following word', () => {
+ const result = appendContentWarning('additionalnot', 'additional');
+
+ expect(result).toBe('additionalnot, additional');
+ });
+
+ it('should append additional when it exists in original multiple times but has preceeding or following word', () => {
+ const result = appendContentWarning('notadditional additionalnot', 'additional');
+
+ expect(result).toBe('notadditional additionalnot, additional');
+ });
+
+ it('should not append additional when it exists in original', () => {
+ const result = appendContentWarning('an additional word', 'additional');
+
+ expect(result).toBe('an additional word');
+ });
+
+ it('should not append additional when original starts with it', () => {
+ const result = appendContentWarning('additional word', 'additional');
+
+ expect(result).toBe('additional word');
+ });
+
+ it('should not append additional when original ends with it', () => {
+ const result = appendContentWarning('an additional', 'additional');
+
+ expect(result).toBe('an additional');
+ });
+
+ it('should not append additional when it appears multiple times', () => {
+ const result = appendContentWarning('an additional additional word', 'additional');
+
+ expect(result).toBe('an additional additional word');
+ });
+
+ it('should not append additional when it appears multiple times but some have preceeding or following', () => {
+ const result = appendContentWarning('a notadditional additional additionalnot word', 'additional');
+
+ expect(result).toBe('a notadditional additional additionalnot word');
+ });
+
+ it('should prepend additional when reverse is true', () => {
+ const result = appendContentWarning('original', 'additional', true);
+
+ expect(result).toBe('additional, original');
+ });
+});
diff --git a/packages/backend/test/unit/misc/check-against-url.ts b/packages/backend/test/unit/misc/check-against-url.ts
deleted file mode 100644
index 70ee957ab1..0000000000
--- a/packages/backend/test/unit/misc/check-against-url.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SPDX-FileCopyrightText: dakkar and sharkey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { describe, expect, test } from '@jest/globals';
-import type { IObject } from '@/core/activitypub/type.js';
-import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
-
-function assertOne(activity: IObject, good = 'http://good') {
- // return a function so we can use `.toThrow`
- return () => assertActivityMatchesUrls(activity, [good]);
-}
-
-describe('assertActivityMatchesUrls', () => {
- it('should throw when no ids are URLs', () => {
- expect(assertOne({ type: 'Test', id: 'bad' }, 'bad')).toThrow(/bad Activity/);
- });
-
- test('id', () => {
- expect(assertOne({ type: 'Test', id: 'http://bad' })).toThrow(/bad Activity/);
- expect(assertOne({ type: 'Test', id: 'http://good' })).not.toThrow();
- });
-
- test('simple url', () => {
- expect(assertOne({ type: 'Test', url: 'http://bad' })).toThrow(/bad Activity/);
- expect(assertOne({ type: 'Test', url: 'http://good' })).not.toThrow();
- });
-
- test('array of urls', () => {
- expect(assertOne({ type: 'Test', url: ['http://bad'] })).toThrow(/bad Activity/);
- expect(assertOne({ type: 'Test', url: ['http://bad', 'http://other'] })).toThrow(/bad Activity/);
- expect(assertOne({ type: 'Test', url: ['http://good'] })).not.toThrow();
- expect(assertOne({ type: 'Test', url: ['http://bad', 'http://good'] })).not.toThrow();
- });
-
- test('array of objects', () => {
- expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }] })).toThrow(/bad Activity/);
- expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, { type: 'Test', href: 'http://other' }] })).toThrow(/bad Activity/);
- expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://good' }] })).not.toThrow();
- expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, { type: 'Test', href: 'http://good' }] })).not.toThrow();
- });
-
- test('mixed array', () => {
- expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, 'http://other'] })).toThrow(/bad Activity/);
- expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, 'http://good'] })).not.toThrow();
- expect(assertOne({ type: 'Test', url: ['http://bad', { type: 'Test', href: 'http://good' }] })).not.toThrow();
- });
-
- test('id and url', () => {
- expect(assertOne({ type: 'Test', id: 'http://other', url: 'http://bad' })).toThrow(/bad Activity/);
- expect(assertOne({ type: 'Test', id: 'http://bad', url: 'http://good' })).not.toThrow();
- expect(assertOne({ type: 'Test', id: 'http://good', url: 'http://bad' })).not.toThrow();
- });
-});
diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts
index 1baa995f59..24cd2236bb 100644
--- a/packages/backend/test/unit/misc/is-renote.ts
+++ b/packages/backend/test/unit/misc/is-renote.ts
@@ -44,6 +44,7 @@ const base: MiNote = {
replyUserHost: null,
renoteUserId: null,
renoteUserHost: null,
+ processErrors: [],
};
describe('misc:is-renote', () => {
diff --git a/packages/backend/test/unit/misc/is-retryable-error.ts b/packages/backend/test/unit/misc/is-retryable-error.ts
new file mode 100644
index 0000000000..096bf64d4f
--- /dev/null
+++ b/packages/backend/test/unit/misc/is-retryable-error.ts
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { UnrecoverableError } from 'bullmq';
+import { AbortError } from 'node-fetch';
+import { isRetryableError } from '@/misc/is-retryable-error.js';
+import { StatusError } from '@/misc/status-error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+
+describe(isRetryableError, () => {
+ it('should return true for retryable StatusError', () => {
+ const error = new StatusError('test error', 500);
+ const result = isRetryableError(error);
+ expect(result).toBeTruthy();
+ });
+
+ it('should return false for permanent StatusError', () => {
+ const error = new StatusError('test error', 400);
+ const result = isRetryableError(error);
+ expect(result).toBeFalsy();
+ });
+
+ it('should return true for retryable IdentifiableError', () => {
+ const error = new IdentifiableError('id', 'message', true);
+ const result = isRetryableError(error);
+ expect(result).toBeTruthy();
+ });
+
+ it('should return false for permanent StatusError', () => {
+ const error = new IdentifiableError('id', 'message', false);
+ const result = isRetryableError(error);
+ expect(result).toBeFalsy();
+ });
+
+ it('should return false for UnrecoverableError', () => {
+ const error = new UnrecoverableError();
+ const result = isRetryableError(error);
+ expect(result).toBeFalsy();
+ });
+
+ it('should return true for typed AbortError', () => {
+ const error = new AbortError();
+ const result = isRetryableError(error);
+ expect(result).toBeTruthy();
+ });
+
+ it('should return true for named AbortError', () => {
+ const error = new Error();
+ error.name = 'AbortError';
+
+ const result = isRetryableError(error);
+
+ expect(result).toBeTruthy();
+ });
+
+ const nonErrorInputs = [
+ [null, 'null'],
+ [undefined, 'undefined'],
+ [0, 'number'],
+ ['string', 'string'],
+ [true, 'boolean'],
+ [[], 'array'],
+ [{}, 'object'],
+ ];
+ for (const [input, label] of nonErrorInputs) {
+ it(`should return true for ${label} input`, () => {
+ const result = isRetryableError(input);
+ expect(result).toBeTruthy();
+ });
+ }
+});
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
index 1506283a3c..d96e6b916a 100644
--- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -18,6 +18,7 @@ import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
import { EmailService } from '@/core/EmailService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { SystemWebhookEventType } from '@/models/SystemWebhook.js';
const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
@@ -334,9 +335,10 @@ describe('CheckModeratorsActivityProcessorService', () => {
mockModeratorRole([user1]);
await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
- expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2);
- expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1);
- expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2);
+ // typeとactiveによる絞り込みが機能しているかはSystemWebhookServiceのテストで確認する.
+ // ここでは呼び出されているか、typeが正しいかのみを確認する
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0] as SystemWebhookEventType).toEqual('inactiveModeratorsWarning');
});
});
@@ -372,8 +374,10 @@ describe('CheckModeratorsActivityProcessorService', () => {
mockModeratorRole([user1]);
await service.notifyChangeToInvitationOnly();
+ // typeとactiveによる絞り込みが機能しているかはSystemWebhookServiceのテストで確認する.
+ // ここでは呼び出されているか、typeが正しいかのみを確認する
expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
- expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0] as SystemWebhookEventType).toEqual('inactiveModeratorsInvitationOnlyChanged');
});
});
});
diff --git a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts
index d13dbd2a71..b1f100698b 100644
--- a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts
+++ b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts
@@ -4,7 +4,9 @@
*/
import type Redis from 'ioredis';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import type { MiUser } from '@/models/User.js';
+import type { RolePolicies, RoleService } from '@/core/RoleService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { BucketRateLimit, Keyed, LegacyRateLimit } from '@/misc/rate-limit-utils.js';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
@@ -15,6 +17,8 @@ describe(SkRateLimiterService, () => {
let mockRedisExec: (batch: [string, ...unknown[]][]) => Promise<[Error | null, unknown][] | null> = null!;
let mockEnvironment: Record<string, string | undefined> = null!;
let serviceUnderTest: () => SkRateLimiterService = null!;
+ let mockDefaultUserPolicies: Partial<RolePolicies> = null!;
+ let mockUserPolicies: Record<string, Partial<RolePolicies>> = null!;
beforeEach(() => {
mockTimeService = {
@@ -69,9 +73,18 @@ describe(SkRateLimiterService, () => {
env: mockEnvironment,
};
+ mockDefaultUserPolicies = { rateLimitFactor: 1 };
+ mockUserPolicies = {};
+ const mockRoleService = {
+ getUserPolicies(key: string | null) {
+ const policies = key != null ? mockUserPolicies[key] : null;
+ return Promise.resolve(policies ?? mockDefaultUserPolicies);
+ },
+ } as unknown as RoleService;
+
let service: SkRateLimiterService | undefined = undefined;
serviceUnderTest = () => {
- return service ??= new SkRateLimiterService(mockTimeService, mockRedisClient, mockEnvService);
+ return service ??= new SkRateLimiterService(mockTimeService, mockRedisClient, mockRoleService, mockEnvService);
};
});
@@ -284,11 +297,12 @@ describe(SkRateLimiterService, () => {
});
it('should scale limit by factor', async () => {
+ mockDefaultUserPolicies.rateLimitFactor = 0.5;
limitCounter = 1;
limitTimestamp = 0;
- const i1 = await serviceUnderTest().limit(limit, actor, 0.5); // 1 + 1 = 2
- const i2 = await serviceUnderTest().limit(limit, actor, 0.5); // 2 + 1 = 3
+ const i1 = await serviceUnderTest().limit(limit, actor); // 1 + 1 = 2
+ const i2 = await serviceUnderTest().limit(limit, actor); // 2 + 1 = 3
expect(i1.blocked).toBeFalsy();
expect(i2.blocked).toBeTruthy();
@@ -330,14 +344,18 @@ describe(SkRateLimiterService, () => {
});
it('should skip if factor is zero', async () => {
- const info = await serviceUnderTest().limit(limit, actor, 0);
+ mockDefaultUserPolicies.rateLimitFactor = 0;
+
+ const info = await serviceUnderTest().limit(limit, actor);
expect(info.blocked).toBeFalsy();
expect(info.remaining).toBe(Number.MAX_SAFE_INTEGER);
});
it('should throw if factor is negative', async () => {
- const promise = serviceUnderTest().limit(limit, actor, -1);
+ mockDefaultUserPolicies.rateLimitFactor = -1;
+
+ const promise = serviceUnderTest().limit(limit, actor);
await expect(promise).rejects.toThrow(/factor is zero or negative/);
});
@@ -426,6 +444,19 @@ describe(SkRateLimiterService, () => {
expect(info.fullResetMs).toBe(2000);
expect(info.fullResetSec).toBe(2);
});
+
+ it('should look up factor by user ID', async () => {
+ const userActor = { id: actor } as unknown as MiUser;
+ mockUserPolicies[actor] = { rateLimitFactor: 0.5 };
+ limitCounter = 1;
+ limitTimestamp = 0;
+
+ const i1 = await serviceUnderTest().limit(limit, userActor); // 1 + 1 = 2
+ const i2 = await serviceUnderTest().limit(limit, userActor); // 2 + 1 = 3
+
+ expect(i1.blocked).toBeFalsy();
+ expect(i2.blocked).toBeTruthy();
+ });
});
describe('with min interval', () => {
@@ -529,11 +560,12 @@ describe(SkRateLimiterService, () => {
});
it('should scale interval by factor', async () => {
+ mockDefaultUserPolicies.rateLimitFactor = 0.5;
limitCounter = 1;
limitTimestamp = 0;
mockTimeService.now += 500;
- const info = await serviceUnderTest().limit(limit, actor, 0.5);
+ const info = await serviceUnderTest().limit(limit, actor);
expect(info.blocked).toBeFalsy();
});
@@ -574,14 +606,18 @@ describe(SkRateLimiterService, () => {
});
it('should skip if factor is zero', async () => {
- const info = await serviceUnderTest().limit(limit, actor, 0);
+ mockDefaultUserPolicies.rateLimitFactor = 0;
+
+ const info = await serviceUnderTest().limit(limit, actor);
expect(info.blocked).toBeFalsy();
expect(info.remaining).toBe(Number.MAX_SAFE_INTEGER);
});
it('should throw if factor is negative', async () => {
- const promise = serviceUnderTest().limit(limit, actor, -1);
+ mockDefaultUserPolicies.rateLimitFactor = -1;
+
+ const promise = serviceUnderTest().limit(limit, actor);
await expect(promise).rejects.toThrow(/factor is zero or negative/);
});
@@ -701,10 +737,11 @@ describe(SkRateLimiterService, () => {
});
it('should scale limit by factor', async () => {
+ mockDefaultUserPolicies.rateLimitFactor = 0.5;
limitCounter = 10;
limitTimestamp = 0;
- const info = await serviceUnderTest().limit(limit, actor, 0.5); // 10 + 1 = 11
+ const info = await serviceUnderTest().limit(limit, actor); // 10 + 1 = 11
expect(info.blocked).toBeTruthy();
});
@@ -760,14 +797,18 @@ describe(SkRateLimiterService, () => {
});
it('should skip if factor is zero', async () => {
- const info = await serviceUnderTest().limit(limit, actor, 0);
+ mockDefaultUserPolicies.rateLimitFactor = 0;
+
+ const info = await serviceUnderTest().limit(limit, actor);
expect(info.blocked).toBeFalsy();
expect(info.remaining).toBe(Number.MAX_SAFE_INTEGER);
});
it('should throw if factor is negative', async () => {
- const promise = serviceUnderTest().limit(limit, actor, -1);
+ mockDefaultUserPolicies.rateLimitFactor = -1;
+
+ const promise = serviceUnderTest().limit(limit, actor);
await expect(promise).rejects.toThrow(/factor is zero or negative/);
});
@@ -890,11 +931,12 @@ describe(SkRateLimiterService, () => {
});
it('should scale limit and interval by factor', async () => {
+ mockDefaultUserPolicies.rateLimitFactor = 0.5;
limitCounter = 5;
limitTimestamp = 0;
mockTimeService.now += 500;
- const info = await serviceUnderTest().limit(limit, actor, 0.5);
+ const info = await serviceUnderTest().limit(limit, actor);
expect(info.blocked).toBeFalsy();
});
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 528faf0a85..163e6096f8 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -4,7 +4,6 @@
"type": "module",
"scripts": {
"watch": "vite",
- "dev": "vite --config vite.config.local-dev.ts --debug hmr",
"build": "vite build",
"typecheck": "vue-tsc --noEmit",
"eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache",
@@ -25,7 +24,7 @@
"estree-walker": "3.0.3",
"misskey-js": "workspace:*",
"frontend-shared": "workspace:*",
- "punycode": "2.3.1",
+ "punycode.js": "2.3.1",
"rollup": "4.26.0",
"sass": "1.79.4",
"shiki": "1.22.2",
@@ -44,7 +43,7 @@
"@types/estree": "1.0.6",
"@types/micromatch": "4.0.9",
"@types/node": "22.9.0",
- "@types/punycode": "2.1.4",
+ "@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.13",
diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts
index 71a3156311..7c8336ce3f 100644
--- a/packages/frontend-embed/src/boot.ts
+++ b/packages/frontend-embed/src/boot.ts
@@ -15,11 +15,11 @@ import { applyTheme, assertIsTheme } from '@/theme.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { DI } from '@/di.js';
import { serverMetadata } from '@/server-metadata.js';
-import { url } from '@@/js/config.js';
+import { url, version, locale, lang, updateLocale, langsVersion } from '@@/js/config.js';
import { parseEmbedParams } from '@@/js/embed-page.js';
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
import { serverContext } from '@/server-context.js';
-import { i18n } from '@/i18n.js';
+import { i18n, updateI18n } from '@/i18n.js';
import type { Theme } from '@/theme.js';
@@ -69,6 +69,22 @@ if (embedParams.colorMode === 'dark') {
}
//#endregion
+//#region Detect language & fetch translations
+const localeVersion = localStorage.getItem('localeVersion');
+const localeOutdated = (localeVersion == null || localeVersion !== langsVersion || locale == null);
+if (localeOutdated) {
+ const res = await window.fetch(`/assets/locales/${lang}.${langsVersion}.json`);
+ if (res.status === 200) {
+ const newLocale = await res.text();
+ const parsedNewLocale = JSON.parse(newLocale);
+ localStorage.setItem('locale', newLocale);
+ localStorage.setItem('localeVersion', langsVersion);
+ updateLocale(parsedNewLocale);
+ updateI18n(parsedNewLocale);
+ }
+}
+//#endregion
+
// サイズの制限
document.documentElement.style.maxWidth = '500px';
diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue
index 6856b8272e..ff794d9b6e 100644
--- a/packages/frontend-embed/src/components/EmAcct.vue
+++ b/packages/frontend-embed/src/components/EmAcct.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { toUnicode } from 'punycode/';
+import { toUnicode } from 'punycode.js';
import { host as hostRaw } from '@@/js/config.js';
defineProps<{
diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue
index a71364237d..b5aaa95894 100644
--- a/packages/frontend-embed/src/components/EmMention.vue
+++ b/packages/frontend-embed/src/components/EmMention.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { toUnicode } from 'punycode';
+import { toUnicode } from 'punycode.js';
import { } from 'vue';
import tinycolor from 'tinycolor2';
import { host as localHost } from '@@/js/config.js';
diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
index 025c4c0734..bf96c557ea 100644
--- a/packages/frontend-embed/src/components/EmNote.vue
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -46,11 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<EmNoteHeader :note="appearNote" :mini="true"/>
<EmInstanceTicker v-if="appearNote.user.instance != null" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
- <p v-if="appearNote.cw != null" :class="$style.cw">
- <EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'" :isBlock="true"/>
+ <p v-if="mergedCW != null" :class="$style.cw">
+ <EmMfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :author="appearNote.user" :nyaize="'respect'" :isBlock="true"/>
<button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
</p>
- <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
+ <div v-show="mergedCW == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<EmA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA>
@@ -109,6 +109,7 @@ import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
import { shouldCollapsed } from '@@/js/collapsed.js';
import { url } from '@@/js/config.js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import I18n from '@/components/I18n.vue';
import EmNoteSub from '@/components/EmNoteSub.vue';
import EmNoteHeader from '@/components/EmNoteHeader.vue';
@@ -154,6 +155,8 @@ const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value
const isLong = shouldCollapsed(appearNote.value, []);
const collapsed = ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
+
+const mergedCW = computed(() => computeMergedCw(appearNote.value));
</script>
<style lang="scss" module>
diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue
index c4ea9b4f2e..0961b36e35 100644
--- a/packages/frontend-embed/src/components/EmNoteDetailed.vue
+++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue
@@ -58,11 +58,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</header>
<div :class="[$style.noteContent, { [$style.contentCollapsed]: collapsed }]">
- <p v-if="appearNote.cw != null" :class="$style.cw">
- <EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'" :isBlock="true"/>
+ <p v-if="mergedCW != null" :class="$style.cw">
+ <EmMfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :author="appearNote.user" :nyaize="'respect'" :isBlock="true"/>
<button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
</p>
- <div v-show="appearNote.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<EmA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA>
<EmMfm
@@ -130,6 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, inject, ref } from 'vue';
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import I18n from '@/components/I18n.vue';
import EmMediaList from '@/components/EmMediaList.vue';
import EmNoteSub from '@/components/EmNoteSub.vue';
@@ -175,6 +176,8 @@ const isDeleted = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const isLong = shouldCollapsed(appearNote.value, []);
const collapsed = ref(appearNote.value.cw == null && isLong);
+
+const mergedCW = computed(() => computeMergedCw(appearNote.value));
</script>
<style lang="scss" module>
diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue
index 83e73f9870..688758edb6 100644
--- a/packages/frontend-embed/src/components/EmNoteSimple.vue
+++ b/packages/frontend-embed/src/components/EmNoteSimple.vue
@@ -9,11 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.main">
<EmNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div>
- <p v-if="note.cw != null" :class="$style.cw">
- <EmMfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis" :isBlock="true"/>
+ <p v-if="mergedCW != null" :class="$style.cw">
+ <EmMfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis" :isBlock="true"/>
<button style="display: block; width: 100%;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
</p>
- <div v-show="note.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<EmSubNoteContent :class="$style.text" :note="note"/>
</div>
</div>
@@ -22,8 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import { i18n } from '@/i18n.js';
import EmAvatar from '@/components/EmAvatar.vue';
import EmNoteHeader from '@/components/EmNoteHeader.vue';
@@ -35,6 +36,8 @@ const props = defineProps<{
}>();
const showContent = ref(false);
+
+const mergedCW = computed(() => computeMergedCw(props.note));
</script>
<style lang="scss" module>
diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue
index cc379e8281..629f0bffcd 100644
--- a/packages/frontend-embed/src/components/EmNoteSub.vue
+++ b/packages/frontend-embed/src/components/EmNoteSub.vue
@@ -11,11 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.body">
<EmNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div>
- <p v-if="note.cw != null" :class="$style.cw">
- <EmMfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :isBlock="true"/>
+ <p v-if="mergedCW != null" :class="$style.cw">
+ <EmMfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :author="note.user" :nyaize="'respect'" :isBlock="true"/>
<button style="display: block; width: 100%;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
</p>
- <div v-show="note.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<EmSubNoteContent :class="$style.text" :note="note"/>
</div>
</div>
@@ -31,8 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import EmA from '@/components/EmA.vue';
import EmAvatar from '@/components/EmAvatar.vue';
import EmNoteHeader from '@/components/EmNoteHeader.vue';
@@ -55,6 +56,8 @@ const props = withDefaults(defineProps<{
const showContent = ref(false);
const replies = ref<Misskey.entities.Note[]>([]);
+const mergedCW = computed(() => computeMergedCw(props.note));
+
if (props.detail) {
misskeyApi('notes/children', {
noteId: props.note.id,
diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue
index 4211261e19..4e0ae005df 100644
--- a/packages/frontend-embed/src/components/EmNotes.vue
+++ b/packages/frontend-embed/src/components/EmNotes.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notes }">
<div :class="[$style.root]">
- <EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
+ <EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note as Misskey.entities.Note"/>
</div>
</template>
</EmPagination>
@@ -24,6 +24,7 @@ import { useTemplateRef } from 'vue';
import EmNote from '@/components/EmNote.vue';
import EmPagination, { Paging } from '@/components/EmPagination.vue';
import { i18n } from '@/i18n.js';
+import * as Misskey from 'misskey-js';
withDefaults(defineProps<{
pagination: Paging;
diff --git a/packages/frontend-embed/src/components/EmUrl.vue b/packages/frontend-embed/src/components/EmUrl.vue
index 94424cab28..2dbbe90858 100644
--- a/packages/frontend-embed/src/components/EmUrl.vue
+++ b/packages/frontend-embed/src/components/EmUrl.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import { toUnicode as decodePunycode } from 'punycode/';
+import { toUnicode as decodePunycode } from 'punycode.js';
import EmA from './EmA.vue';
import { url as local } from '@@/js/config.js';
diff --git a/packages/frontend-embed/src/index.html b/packages/frontend-embed/src/index.html
deleted file mode 100644
index d94ada5ea8..0000000000
--- a/packages/frontend-embed/src/index.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!--
- SPDX-FileCopyrightText: syuilo and misskey-project
- SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<!--
- 開発モードのviteはこのファイルを起点にサーバーを起動します。
- このファイルに書かれた [t]js のリンクと (s)cssのリンクと、その依存関係にあるファイルはビルドされます
--->
-
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="UTF-8" />
- <title>[DEV] Loading...</title>
- <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
- <meta
- http-equiv="Content-Security-Policy"
- content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
- worker-src 'self';
- script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh https://cdn.jsdelivr.net;
- style-src 'self' 'unsafe-inline';
- img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
- media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
- connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
- frame-src *;"
- />
- <meta property="og:site_name" content="[DEV BUILD] Sharkey" />
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel='stylesheet' href='/assets/phosphor-icons/bold/style.css'>
- <link rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css'>
-</head>
-
-<body>
-<div id="sharkey_app"></div>
-<script type="module" src="./boot.ts"></script>
-</body>
-</html>
diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts
index 4664ad4880..680ab80167 100644
--- a/packages/frontend-embed/src/theme.ts
+++ b/packages/frontend-embed/src/theme.ts
@@ -75,16 +75,21 @@ function compile(theme: Theme): Record<string, string> {
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
- const func = parts.shift().substring(1);
- const arg = parseFloat(parts.shift());
- const color = getColor(parts.join('<'));
+ const funcTxt = parts.shift();
+ const argTxt = parts.shift();
- switch (func) {
- case 'darken': return color.darken(arg);
- case 'lighten': return color.lighten(arg);
- case 'alpha': return color.setAlpha(arg);
- case 'hue': return color.spin(arg);
- case 'saturate': return color.saturate(arg);
+ if (funcTxt && argTxt) {
+ const func = funcTxt.substring(1);
+ const arg = parseFloat(argTxt);
+ const color = getColor(parts.join('<'));
+
+ switch (func) {
+ case 'darken': return color.darken(arg);
+ case 'lighten': return color.lighten(arg);
+ case 'alpha': return color.setAlpha(arg);
+ case 'hue': return color.spin(arg);
+ case 'saturate': return color.saturate(arg);
+ }
}
}
diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json
index 1af34f378c..ff04a689bf 100644
--- a/packages/frontend-embed/tsconfig.json
+++ b/packages/frontend-embed/tsconfig.json
@@ -10,8 +10,8 @@
"declaration": false,
"sourceMap": false,
"target": "ES2022",
- "module": "nodenext",
- "moduleResolution": "nodenext",
+ "module": "ES2022",
+ "moduleResolution": "Bundler",
"removeComments": false,
"noLib": false,
"strict": true,
diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts
deleted file mode 100644
index bf2f478887..0000000000
--- a/packages/frontend-embed/vite.config.local-dev.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import dns from 'dns';
-import { readFile } from 'node:fs/promises';
-import type { IncomingMessage } from 'node:http';
-import { defineConfig } from 'vite';
-import type { UserConfig } from 'vite';
-import * as yaml from 'js-yaml';
-import locales from '../../locales/index.js';
-import { getConfig } from './vite.config.js';
-
-dns.setDefaultResultOrder('ipv4first');
-
-const defaultConfig = getConfig();
-
-const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
-
-const httpUrl = `http://localhost:${port}/`;
-const websocketUrl = `ws://localhost:${port}/`;
-
-// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す
-function varyHandler(req: IncomingMessage) {
- if (req.headers.accept?.includes('application/activity+json')) {
- return null;
- }
- return '/index.html';
-}
-
-const devConfig: UserConfig = {
- // 基本の設定は vite.config.js から引き継ぐ
- ...defaultConfig,
- root: 'src',
- publicDir: '../assets',
- base: '/embed',
- server: {
- host: 'localhost',
- port: 5174,
- proxy: {
- '/api': {
- changeOrigin: true,
- target: httpUrl,
- },
- '/assets': httpUrl,
- '/static-assets': httpUrl,
- '/client-assets': httpUrl,
- '/files': httpUrl,
- '/twemoji': httpUrl,
- '/fluent-emoji': httpUrl,
- '/sw.js': httpUrl,
- '/streaming': {
- target: websocketUrl,
- ws: true,
- },
- '/favicon.ico': httpUrl,
- '/robots.txt': httpUrl,
- '/embed.js': httpUrl,
- '/identicon': {
- target: httpUrl,
- rewrite(path) {
- return path.replace('@localhost:5173', '');
- },
- },
- '/url': httpUrl,
- '/proxy': httpUrl,
- '/_info_card_': httpUrl,
- '/bios': httpUrl,
- '/cli': httpUrl,
- '/inbox': httpUrl,
- '/emoji/': httpUrl,
- '/notes': {
- target: httpUrl,
- bypass: varyHandler,
- },
- '/users': {
- target: httpUrl,
- bypass: varyHandler,
- },
- '/.well-known': {
- target: httpUrl,
- },
- },
- },
- build: {
- ...defaultConfig.build,
- rollupOptions: {
- ...defaultConfig.build?.rollupOptions,
- input: 'index.html',
- },
- },
-
- define: {
- ...defaultConfig.define,
- _LANGS_FULL_: JSON.stringify(Object.entries(locales)),
- },
-};
-
-export default defineConfig(({ command, mode }) => devConfig);
-
diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
index bf6f558893..1cd47b2754 100644
--- a/packages/frontend-embed/vite.config.ts
+++ b/packages/frontend-embed/vite.config.ts
@@ -2,6 +2,7 @@ import path from 'path';
import pluginVue from '@vitejs/plugin-vue';
import { type UserConfig, defineConfig } from 'vite';
import { localesVersion } from '../../locales/version.js';
+
import locales from '../../locales/index.js';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
@@ -64,6 +65,12 @@ export function getConfig(): UserConfig {
server: {
port: 5174,
+ hmr: {
+ // バックエンド経由での起動時、Viteは5174経由でアセットを参照していると思い込んでいるが実際は3000から配信される
+ // そのため、バックエンドのWSサーバーにHMRのWSリクエストが吸収されてしまい、正しくHMRが機能しない
+ // クライアント側のWSポートをViteサーバーのポートに強制させることで、正しくHMRが機能するようになる
+ clientPort: 5174,
+ },
},
plugins: [
diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js
index 17b6da8d30..9941114757 100644
--- a/packages/frontend-shared/build.js
+++ b/packages/frontend-shared/build.js
@@ -23,10 +23,14 @@ const options = {
sourcemap: 'linked',
};
+const args = process.argv.slice(2).map(arg => arg.toLowerCase());
+
// js-built配下をすべて削除する
-fs.rmSync('./js-built', { recursive: true, force: true });
+if (!args.includes('--no-clean')) {
+ fs.rmSync('./js-built', { recursive: true, force: true });
+}
-if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
+if (args.includes('--watch')) {
await watchSrc();
} else {
await buildSrc();
diff --git a/packages/frontend-shared/js/append-content-warning.ts b/packages/frontend-shared/js/append-content-warning.ts
new file mode 100644
index 0000000000..7f24a66f23
--- /dev/null
+++ b/packages/frontend-shared/js/append-content-warning.ts
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/*
+ * Important Note: this file must be kept in sync with packages/backend/src/misc/append-content-warning.ts
+ */
+
+/**
+ * Appends an additional content warning onto an existing one.
+ * The additional value will not be added if it already exists within the original input.
+ * @param original Existing content warning
+ * @param additional Content warning to append
+ * @param reverse If true, then the additional CW will be prepended instead of appended.
+ */
+export function appendContentWarning(original: string | null | undefined, additional: string, reverse = false): string {
+ // Easy case - if original is empty, then additional replaces it.
+ if (!original) {
+ return additional;
+ }
+
+ // Easy case - if the additional CW is empty, then don't append it.
+ if (!additional) {
+ return original;
+ }
+
+ // If the additional CW already exists in the input, then we *don't* append another copy!
+ if (includesWholeWord(original, additional)) {
+ return original;
+ }
+
+ return reverse
+ ? `${additional}, ${original}`
+ : `${original}, ${additional}`;
+}
+
+/**
+ * Emulates a regular expression like /\b(pattern)\b/, but with a raw non-regex pattern.
+ * We're checking to see whether the default CW appears inside the existing CW, but *only* if there's word boundaries on either side.
+ * @param input Input string to search
+ * @param target Target word / phrase to search for
+ */
+function includesWholeWord(input: string, target: string): boolean {
+ const parts = input.split(target);
+
+ // The additional string could appear multiple times within the original input.
+ // We need to check each occurrence, since any of them could potentially match.
+ for (let i = 0; i + 1 < parts.length; i++) {
+ const before = parts[i];
+ const after = parts[i + 1];
+
+ // If either the preceding or following tokens are a "word", then this "match" is actually just part of a longer word.
+ // Likewise, if *neither* token is a word, then this is a real match and the CW already exists in the input.
+ if (!/\w$/.test(before) && !/^\w/.test(after)) {
+ return true;
+ }
+ }
+
+ // If we don't match, then there is no existing CW.
+ return false;
+}
diff --git a/packages/frontend-shared/js/compute-merged-cw.ts b/packages/frontend-shared/js/compute-merged-cw.ts
new file mode 100644
index 0000000000..dfea57fdce
--- /dev/null
+++ b/packages/frontend-shared/js/compute-merged-cw.ts
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { appendContentWarning } from '@@/js/append-content-warning.js';
+
+export function computeMergedCw(note: Misskey.entities.Note): string | null {
+ let cw = note.cw;
+
+ if (note.user.mandatoryCW) {
+ cw = appendContentWarning(cw, note.user.mandatoryCW);
+ }
+
+ return cw ?? null;
+}
diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json
index 8bf25da161..6afd4e8a23 100644
--- a/packages/frontend-shared/package.json
+++ b/packages/frontend-shared/package.json
@@ -26,6 +26,7 @@
"@typescript-eslint/parser": "7.17.0",
"esbuild": "0.24.0",
"eslint-plugin-vue": "9.31.0",
+ "nodemon": "3.1.7",
"typescript": "5.6.3",
"vue-eslint-parser": "9.4.3"
},
@@ -34,6 +35,7 @@
],
"dependencies": {
"misskey-js": "workspace:*",
+ "nodemon": "3.1.7",
"vue": "3.5.12"
}
}
diff --git a/packages/frontend/.storybook/fake-utils.ts b/packages/frontend/.storybook/fake-utils.ts
new file mode 100644
index 0000000000..c777cbbe72
--- /dev/null
+++ b/packages/frontend/.storybook/fake-utils.ts
@@ -0,0 +1,154 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import seedrandom from 'seedrandom';
+
+/**
+ * AIで生成した無作為なファーストネーム
+ */
+export const firstNameDict = [
+ 'Ethan', 'Olivia', 'Jackson', 'Emma', 'Liam', 'Ava', 'Aiden', 'Sophia', 'Mason', 'Isabella',
+ 'Noah', 'Mia', 'Lucas', 'Harper', 'Caleb', 'Abigail', 'Samuel', 'Emily', 'Logan',
+ 'Madison', 'Benjamin', 'Chloe', 'Elijah', 'Grace', 'Alexander', 'Scarlett', 'William', 'Zoey', 'James', 'Lily',
+]
+
+/**
+ * AIで生成した無作為なラストネーム
+ */
+export const lastNameDict = [
+ 'Anderson', 'Johnson', 'Thompson', 'Davis', 'Rodriguez', 'Smith', 'Patel', 'Williams', 'Lee', 'Brown',
+ 'Garcia', 'Jackson', 'Martinez', 'Taylor', 'Harris', 'Nguyen', 'Miller', 'Jones', 'Wilson',
+ 'White', 'Thomas', 'Garcia', 'Martinez', 'Robinson', 'Turner', 'Lewis', 'Hall', 'King', 'Baker', 'Cooper',
+]
+
+/**
+ * AIで生成した無作為な国名
+ */
+export const countryDict = [
+ 'Japan', 'Canada', 'Brazil', 'Australia', 'Italy', 'SouthAfrica', 'Mexico', 'Sweden', 'Russia', 'India',
+ 'Germany', 'Argentina', 'South Korea', 'France', 'Nigeria', 'Turkey', 'Spain', 'Egypt', 'Thailand',
+ 'Vietnam', 'Kenya', 'Saudi Arabia', 'Netherlands', 'Colombia', 'Poland', 'Chile', 'Malaysia', 'Ukraine', 'New Zealand', 'Peru',
+]
+
+export function text(length: number = 10, seed?: string): string {
+ let result = "";
+
+ // シード値を使う場合、同じ数値が羅列されるが、ランダム文字列という意味では満たせていると思うのでこのまま使っておく
+ const rand = seed ? seedrandom(seed)() : Math.random();
+ while (result.length < length) {
+ result += rand.toString(36).substring(2);
+ }
+
+ return result.substring(0, length);
+}
+
+export function integer(min: number = 0, max: number = 9999, seed?: string): number {
+ const rand = seed ? seedrandom(seed)() : Math.random();
+ return Math.floor(rand * (max - min)) + min;
+}
+
+export function date(params?: {
+ yearMin?: number,
+ yearMax?: number,
+ monthMin?: number,
+ monthMax?: number,
+ dayMin?: number,
+ dayMax?: number,
+ hourMin?: number,
+ hourMax?: number,
+ minuteMin?: number,
+ minuteMax?: number,
+ secondMin?: number,
+ secondMax?: number,
+ millisecondMin?: number,
+ millisecondMax?: number,
+}, seed?: string): Date {
+ const year = integer(params?.yearMin ?? 1970, params?.yearMax ?? (new Date()).getFullYear(), seed);
+ const month = integer(params?.monthMin ?? 1, params?.monthMax ?? 12, seed);
+ let day = integer(params?.dayMin ?? 1, params?.dayMax ?? 31, seed);
+ if (month === 2) {
+ day = Math.min(day, 28);
+ } else if ([4, 6, 9, 11].includes(month)) {
+ day = Math.min(day, 30);
+ } else {
+ day = Math.min(day, 31);
+ }
+
+ const hour = integer(params?.hourMin ?? 0, params?.hourMax ?? 23, seed);
+ const minute = integer(params?.minuteMin ?? 0, params?.minuteMax ?? 59, seed);
+ const second = integer(params?.secondMin ?? 0, params?.secondMax ?? 59, seed);
+ const millisecond = integer(params?.millisecondMin ?? 0, params?.millisecondMax ?? 999, seed);
+
+ return new Date(year, month - 1, day, hour, minute, second, millisecond);
+}
+
+export function boolean(seed?: string): boolean {
+ const rand = seed ? seedrandom(seed)() : Math.random();
+ return rand < 0.5;
+}
+
+export function choose<T>(array: T[], seed?: string): T {
+ const rand = seed ? seedrandom(seed)() : Math.random();
+ return array[Math.floor(rand * array.length)];
+}
+
+export function firstName(seed?: string): string {
+ return choose(firstNameDict, seed);
+}
+
+export function lastName(seed?: string): string {
+ return choose(lastNameDict, seed);
+}
+
+export function country(seed?: string): string {
+ return choose(countryDict, seed);
+}
+
+const TIME2000 = 946684800000;
+export function fakeId(seed?: string): string {
+ let time = new Date().getTime();
+
+ time = time - TIME2000;
+ if (time < 0) time = 0;
+
+ const timeStr = time.toString(36).padStart(8, '0');
+ const noiseStr = text(2, seed);
+
+ return timeStr + noiseStr;
+}
+
+export function imageDataUrl(options?: {
+ size?: {
+ width?: number,
+ height?: number,
+ },
+ color?: {
+ red?: number,
+ green?: number,
+ blue?: number,
+ alpha?: number,
+ }
+}, seed?: string): string {
+ const canvas = document.createElement('canvas');
+ canvas.width = options?.size?.width ?? 100;
+ canvas.height = options?.size?.height ?? 100;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ throw new Error('Failed to get 2d context');
+ }
+
+ ctx.beginPath()
+
+ const red = options?.color?.red ?? integer(0, 255, seed);
+ const green = options?.color?.green ?? integer(0, 255, seed);
+ const blue = options?.color?.blue ?? integer(0, 255, seed);
+ const alpha = options?.color?.alpha ?? 1;
+ ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2, true);
+ ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${alpha})`;
+ ctx.fill();
+
+ return canvas.toDataURL('image/png', 1.0);
+}
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts
index 758827c196..377d26d6a3 100644
--- a/packages/frontend/.storybook/fakes.ts
+++ b/packages/frontend/.storybook/fakes.ts
@@ -5,6 +5,7 @@
import { AISCRIPT_VERSION } from '@syuilo/aiscript';
import type { entities } from 'misskey-js'
+import { date, imageDataUrl, text } from "./fake-utils.js";
export function abuseUserReport() {
return {
@@ -302,3 +303,93 @@ export function inviteCode(isUsed = false, hasExpiration = false, isExpired = fa
used: isUsed,
}
}
+
+export function role(params: {
+ id?: string,
+ name?: string,
+ color?: string | null,
+ iconUrl?: string | null,
+ description?: string,
+ isModerator?: boolean,
+ isAdministrator?: boolean,
+ displayOrder?: number,
+ createdAt?: string,
+ updatedAt?: string,
+ target?: 'manual' | 'conditional',
+ isPublic?: boolean,
+ isExplorable?: boolean,
+ asBadge?: boolean,
+ canEditMembersByModerator?: boolean,
+ usersCount?: number,
+}, seed?: string): entities.Role {
+ const prefix = params.displayOrder ? params.displayOrder.toString().padStart(3, '0') + '-' : '';
+ const genId = text(36, seed);
+ const createdAt = params.createdAt ?? date({}, seed).toISOString();
+ const updatedAt = params.updatedAt ?? date({}, seed).toISOString();
+
+ return {
+ id: params.id ?? genId,
+ name: params.name ?? `${prefix}TestRole-${genId}`,
+ color: params.color ?? '#445566',
+ iconUrl: params.iconUrl ?? null,
+ description: params.description ?? '',
+ isModerator: params.isModerator ?? false,
+ isAdministrator: params.isAdministrator ?? false,
+ displayOrder: params.displayOrder ?? 0,
+ createdAt: createdAt,
+ updatedAt: updatedAt,
+ target: params.target ?? 'manual',
+ isPublic: params.isPublic ?? true,
+ isExplorable: params.isExplorable ?? true,
+ asBadge: params.asBadge ?? true,
+ canEditMembersByModerator: params.canEditMembersByModerator ?? false,
+ usersCount: params.usersCount ?? 10,
+ condFormula: {
+ id: '',
+ type: 'or',
+ values: []
+ },
+ policies: {},
+ }
+}
+
+export function emoji(params?: {
+ id?: string,
+ name?: string,
+ host?: string,
+ uri?: string,
+ publicUrl?: string,
+ originalUrl?: string,
+ type?: string,
+ aliases?: string[],
+ category?: string,
+ license?: string,
+ isSensitive?: boolean,
+ localOnly?: boolean,
+ roleIdsThatCanBeUsedThisEmojiAsReaction?: {id:string, name:string}[],
+ updatedAt?: string,
+}, seed?: string): entities.EmojiDetailedAdmin {
+ const _seed = seed ?? (params?.id ?? "DEFAULT_SEED");
+ const id = params?.id ?? text(32, _seed);
+ const name = params?.name ?? text(8, _seed);
+ const updatedAt = params?.updatedAt ?? date({}, _seed).toISOString();
+
+ const image = imageDataUrl({}, _seed)
+
+ return {
+ id: id,
+ name: name,
+ host: params?.host ?? null,
+ uri: params?.uri ?? null,
+ publicUrl: params?.publicUrl ?? image,
+ originalUrl: params?.originalUrl ?? image,
+ type: params?.type ?? 'image/png',
+ aliases: params?.aliases ?? [`alias1-${name}`, `alias2-${name}`],
+ category: params?.category ?? null,
+ license: params?.license ?? null,
+ isSensitive: params?.isSensitive ?? false,
+ localOnly: params?.localOnly ?? false,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: params?.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
+ updatedAt: updatedAt,
+ }
+}
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index f2bdc631d2..8830523810 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -416,6 +416,10 @@ function toStories(component: string): Promise<string> {
glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/components/MkInstanceCardMini.vue'),
glob('src/components/MkInviteCode.vue'),
+ glob('src/components/MkTagItem.vue'),
+ glob('src/components/MkRoleSelectDialog.vue'),
+ glob('src/components/grid/MkGrid.vue'),
+ glob('src/pages/admin/custom-emojis-manager2.vue'),
glob('src/pages/admin/overview.ap-requests.vue'),
glob('src/pages/user/home.vue'),
glob('src/pages/search.vue'),
diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js
index e72c18a854..932655d9bd 100644
--- a/packages/frontend/eslint.config.js
+++ b/packages/frontend/eslint.config.js
@@ -30,6 +30,7 @@ export default [
// Misskey
_DEV_: false,
_LANGS_: false,
+ _LANGS_VERSION_: false,
_VERSION_: false,
_ENV_: false,
_PERF_PREFIX_: false,
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 63eaded968..528a1aef34 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -4,7 +4,6 @@
"type": "module",
"scripts": {
"watch": "vite",
- "dev": "vite --config vite.config.local-dev.ts --debug hmr",
"build": "vite build",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
@@ -31,7 +30,7 @@
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.12",
- "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
+ "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"astring": "1.9.0",
"broadcast-channel": "7.0.0",
"buraha": "0.0.1",
@@ -59,7 +58,7 @@
"misskey-reversi": "workspace:*",
"moment": "^2.30.1",
"photoswipe": "5.4.4",
- "punycode": "2.3.1",
+ "punycode.js": "2.3.1",
"rollup": "4.26.0",
"sanitize-html": "2.13.1",
"sass": "1.79.3",
@@ -108,7 +107,7 @@
"@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9",
"@types/node": "22.9.0",
- "@types/punycode": "2.1.4",
+ "@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.13.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts
deleted file mode 100644
index f312765dcf..0000000000
--- a/packages/frontend/src/_dev_boot_.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-await main();
-
-import('@/_boot_.js');
-
-/**
- * backend/src/server/web/boot.jsで差し込まれている起動処理のうち、最低限必要なものを模倣するための処理
- */
-async function main() {
- const forceError = localStorage.getItem('forceError');
- if (forceError != null) {
- renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
- }
-
- //#region Detect language & fetch translations
-
- // dev-modeの場合は常に取り直す
- const supportedLangs = _LANGS_.map(it => it[0]);
- let lang: string | null | undefined = localStorage.getItem('lang');
- if (lang == null || !supportedLangs.includes(lang)) {
- if (supportedLangs.includes(navigator.language)) {
- lang = navigator.language;
- } else {
- lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
-
- // Fallback
- if (lang == null) lang = 'en-US';
- }
- }
-
- // TODO:今のままだと言語ファイル変更後はpnpm devをリスタートする必要があるので、chokidarを使ったり等で対応できるようにする
- const locale = _LANGS_FULL_.find(it => it[0] === lang);
- localStorage.setItem('lang', lang);
- localStorage.setItem('locale', JSON.stringify(locale[1]));
- localStorage.setItem('localeVersion', _VERSION_);
- //#endregion
-
- //#region Theme
- const theme = localStorage.getItem('theme');
- if (theme) {
- for (const [k, v] of Object.entries(JSON.parse(theme))) {
- document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
-
- // HTMLの theme-color 適用
- if (k === 'htmlThemeColor') {
- for (const tag of document.head.children) {
- if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
- tag.setAttribute('content', v);
- break;
- }
- }
- }
- }
- }
- const colorScheme = localStorage.getItem('colorScheme');
- if (colorScheme) {
- document.documentElement.style.setProperty('color-scheme', colorScheme);
- }
- //#endregion
-
- const fontSize = localStorage.getItem('fontSize');
- if (fontSize) {
- document.documentElement.classList.add('f-' + fontSize);
- }
-
- const useSystemFont = localStorage.getItem('useSystemFont');
- if (useSystemFont) {
- document.documentElement.classList.add('useSystemFont');
- }
-
- const wallpaper = localStorage.getItem('wallpaper');
- if (wallpaper) {
- document.documentElement.style.backgroundImage = `url(${wallpaper})`;
- }
-
- const customCss = localStorage.getItem('customCss');
- if (customCss && customCss.length > 0) {
- const style = document.createElement('style');
- style.innerHTML = customCss;
- document.head.appendChild(style);
- }
-}
-
-function renderError(code: string, details?: string) {
- console.log(code, details);
-}
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index 366345b5b3..b3fa151a22 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -102,6 +102,9 @@ export async function removeAccount(idOrToken: Account['id']) {
}
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> {
+ document.cookie = `token=; path=/; max-age=0${ location.protocol === 'https:' ? '; Secure' : ''}`;
+ document.cookie = `token=${token}; path=/queue; max-age=86400${ location.protocol === 'https:' ? '; SameSite=Strict; Secure' : ''}`; // bull dashboardの認証とかで使う
+
return new Promise((done, fail) => {
window.fetch(`${apiUrl}/i`, {
method: 'POST',
@@ -150,9 +153,9 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
} else if (res.error.id === 'd5826d14-3982-4d2e-8011-b9e9f02499ef') {
// rate limited
const timeToWait = res.error.info?.resetMs ?? 1000;
- window.setTimeout(timeToWait, () => {
+ window.setTimeout(() => {
fetchAccount(token, id, forceShowDialog).then(done, fail);
- });
+ }, timeToWait);
return;
} else {
await alert({
@@ -221,7 +224,6 @@ export async function login(token: Account['token'], redirect?: string) {
throw reason;
});
miLocalStorage.setItem('account', JSON.stringify(me));
- document.cookie = `token=${token}; path=/; max-age=31536000${ location.protocol === 'https:' ? '; Secure' : ''}`; // bull dashboardの認証とかで使う
await addAccount(me.id, token);
if (redirect) {
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index d43a2b0799..46ec4533ec 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -99,6 +99,11 @@ export async function common(createVue: () => App<Element>) {
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
+ // URLに#pswpを含む場合は取り除く
+ if (location.hash === '#pswp') {
+ history.replaceState(null, '', location.href.replace('#pswp', ''));
+ }
+
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index eb8a4d30d2..6c544feb2a 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -7,6 +7,7 @@ import { createApp, defineAsyncComponent, markRaw } from 'vue';
import { ui } from '@@/js/config.js';
import { common } from './common.js';
import type * as Misskey from 'misskey-js';
+import type { Component } from 'vue';
import { i18n } from '@/i18n.js';
import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js';
@@ -26,13 +27,38 @@ import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
export async function mainBoot() {
- const { isClientUpdated } = await common(() => createApp(
- new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
- !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
- ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
- ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
- defineAsyncComponent(() => import('@/ui/universal.vue')),
- ));
+ const { isClientUpdated } = await common(() => {
+ let uiStyle = ui;
+ const searchParams = new URLSearchParams(window.location.search);
+
+ if (!$i) uiStyle = 'visitor';
+
+ if (searchParams.has('zen')) uiStyle = 'zen';
+ if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen';
+
+ if (searchParams.has('ui')) uiStyle = searchParams.get('ui');
+
+ let rootComponent: Component;
+ switch (uiStyle) {
+ case 'zen':
+ rootComponent = defineAsyncComponent(() => import('@/ui/zen.vue'));
+ break;
+ case 'deck':
+ rootComponent = defineAsyncComponent(() => import('@/ui/deck.vue'));
+ break;
+ case 'visitor':
+ rootComponent = defineAsyncComponent(() => import('@/ui/visitor.vue'));
+ break;
+ case 'classic':
+ rootComponent = defineAsyncComponent(() => import('@/ui/classic.vue'));
+ break;
+ default:
+ rootComponent = defineAsyncComponent(() => import('@/ui/universal.vue'));
+ break;
+ }
+
+ return createApp(rootComponent);
+ });
reactionPicker.init();
emojiPicker.init();
diff --git a/packages/frontend/src/components/DynamicNote.vue b/packages/frontend/src/components/DynamicNote.vue
new file mode 100644
index 0000000000..6703099591
--- /dev/null
+++ b/packages/frontend/src/components/DynamicNote.vue
@@ -0,0 +1,49 @@
+<!--
+SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<XNote
+ ref="rootEl"
+ :note="note"
+ :pinned="pinned"
+ :mock="mock"
+ :withHardMute="withHardMute"
+ @reaction="emoji => emit('reaction', emoji)"
+ @removeReaction="emoji => emit('removeReaction', emoji)"
+/>
+</template>
+
+<script setup lang="ts">
+import * as Misskey from 'misskey-js';
+import { computed, defineAsyncComponent, shallowRef } from 'vue';
+import type { ComponentExposed } from 'vue-component-type-helpers';
+import type MkNote from '@/components/MkNote.vue';
+import type SkNote from '@/components/SkNote.vue';
+import { defaultStore } from '@/store';
+
+const XNote = computed(() =>
+ defineAsyncComponent(() =>
+ defaultStore.reactiveState.noteDesign.value === 'misskey'
+ ? import('@/components/MkNote.vue')
+ : import('@/components/SkNote.vue'),
+ ),
+);
+
+const rootEl = shallowRef<ComponentExposed<typeof MkNote | typeof SkNote>>();
+
+defineExpose({ rootEl });
+
+defineProps<{
+ note: Misskey.entities.Note;
+ pinned?: boolean;
+ mock?: boolean;
+ withHardMute?: boolean;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'reaction', emoji: string): void;
+ (ev: 'removeReaction', emoji: string): void;
+}>();
+</script>
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index c28dbc7ffa..564d1fe7e3 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
- <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onChange">
+ <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
@@ -77,8 +77,8 @@ import MkPostForm from '@/components/MkPostForm.vue';
const props = withDefaults(defineProps<{
component: AsUiComponent;
components: Ref<AsUiComponent>[];
- size: 'small' | 'medium' | 'large';
- align: 'left' | 'center' | 'right';
+ size?: 'small' | 'medium' | 'large';
+ align?: 'left' | 'center' | 'right';
}>(), {
size: 'medium',
align: 'left',
@@ -86,7 +86,7 @@ const props = withDefaults(defineProps<{
const c = props.component;
-function g(id) {
+function g(id: string) {
const v = props.components.find(x => x.value.id === id)?.value;
if (v) return v;
@@ -122,13 +122,22 @@ const containerStyle = computed(() => {
const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
-function onSwitchUpdate(v) {
+function onSwitchUpdate(v: boolean) {
valueForSwitch.value = v;
if ('onChange' in c && c.onChange) {
c.onChange(v as never);
}
}
+const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null);
+
+function onSelectUpdate(v) {
+ valueForSelect.value = v;
+ if ('onChange' in c && c.onChange) {
+ c.onChange(v as never);
+ }
+}
+
function openPostForm() {
const form = (c as AsUiPostFormButton).form;
if (!form) return;
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index e9493edbd1..aeed90722f 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -30,6 +30,9 @@ import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmount
import { defaultStore } from '@/store.js';
// APIs provided by Captcha services
+// see: https://docs.hcaptcha.com/configuration/#javascript-api
+// see: https://developers.google.com/recaptcha/docs/display?hl=ja
+// see: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget
export type Captcha = {
render(container: string | Node, options: {
readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
@@ -56,6 +59,7 @@ declare global {
const props = defineProps<{
provider: CaptchaProvider;
sitekey: string | null; // null will show error on request
+ secretKey?: string | null;
instanceUrl?: string | null;
modelValue?: string | null;
}>();
@@ -67,7 +71,7 @@ const emit = defineEmits<{
const available = ref(false);
const captchaEl = shallowRef<HTMLDivElement | undefined>();
-
+const captchaWidgetId = ref<string | undefined>(undefined);
const testcaptchaInput = ref('');
const testcaptchaPassed = ref(false);
@@ -99,6 +103,15 @@ const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
+watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => {
+ // 変更があったときはリフレッシュと再レンダリングをしておかないと、変更後の値で再検証が出来ない
+ if (available.value) {
+ callback(undefined);
+ clearWidget();
+ await requestRender();
+ }
+});
+
if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
available.value = true;
} else if (src.value !== null) {
@@ -111,14 +124,38 @@ if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha')
}
function reset() {
- if (captcha.value.reset) captcha.value.reset();
+ if (captcha.value.reset && captchaWidgetId.value !== undefined) {
+ try {
+ captcha.value.reset(captchaWidgetId.value);
+ } catch (error: unknown) {
+ // ignore
+ if (_DEV_) console.warn(error);
+ }
+ }
testcaptchaPassed.value = false;
testcaptchaInput.value = '';
}
+function remove() {
+ if (captcha.value.remove && captchaWidgetId.value) {
+ try {
+ if (_DEV_) console.log('remove', props.provider, captchaWidgetId.value);
+ captcha.value.remove(captchaWidgetId.value);
+ } catch (error: unknown) {
+ // ignore
+ if (_DEV_) console.warn(error);
+ }
+ }
+}
+
async function requestRender() {
- if (captcha.value.render && captchaEl.value instanceof Element) {
- captcha.value.render(captchaEl.value, {
+ if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
+ // reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する.
+ // (同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので)
+ const elem = document.createElement('div');
+ captchaEl.value.appendChild(elem);
+
+ captchaWidgetId.value = captcha.value.render(elem, {
sitekey: props.sitekey,
theme: defaultStore.state.darkMode ? 'dark' : 'light',
callback: callback,
@@ -146,6 +183,23 @@ async function requestRender() {
}
}
+function clearWidget() {
+ if (props.provider === 'mcaptcha') {
+ const container = document.getElementById('mcaptcha__widget-container');
+ if (container) {
+ container.innerHTML = '';
+ }
+ } else {
+ reset();
+ remove();
+
+ if (captchaEl.value) {
+ // レンダリング先のコンテナの中身を掃除し、フォームが増殖するのを抑止
+ captchaEl.value.innerHTML = '';
+ }
+ }
+}
+
function callback(response?: string) {
emit('update:modelValue', typeof response === 'string' ? response : null);
}
@@ -178,7 +232,7 @@ onUnmounted(() => {
});
onBeforeUnmount(() => {
- reset();
+ clearWidget();
});
defineExpose({
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index e036fec528..7ff9da1ced 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -125,7 +125,9 @@ const bannerStyle = computed(() => {
position: absolute;
top: 16px;
left: 16px;
+ max-width: calc(100% - 32px);
padding: 12px 16px;
+ box-sizing: border-box;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 1.2em;
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index f4d20c7d8c..30a9b26bef 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<div v-show="showBody" ref="contentEl" :class="[$style.content, { [$style.omitted]: omitted }]">
<slot></slot>
- <button v-if="omitted" :class="$style.fade" class="_button" @click="() => { ignoreOmit = true; omitted = false; }">
+ <button v-if="omitted" :class="$style.fade" class="_button" @click="showMore">
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
</button>
</div>
@@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{
thin?: boolean;
naked?: boolean;
foldable?: boolean;
+ onUnfold?: () => boolean; // return false to prevent unfolding
scrollable?: boolean;
expanded?: boolean;
maxHeight?: number | null;
@@ -101,6 +102,13 @@ const omitObserver = new ResizeObserver((entries, observer) => {
calcOmit();
});
+function showMore() {
+ if (props.onUnfold && !props.onUnfold()) return;
+
+ ignoreOmit.value = true;
+ omitted.value = false;
+}
+
onMounted(() => {
watch(showBody, v => {
if (!rootEl.value) return;
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
index ecbee864dc..e6ab17417d 100644
--- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { defineProps, shallowRef } from 'vue';
+import { shallowRef } from 'vue';
import MkLink from '@/components/MkLink.vue';
import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue
index 1079e52030..5ba5de0c4a 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -5,13 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- ref="thumbnail"
- :class="[
- $style.root,
- { [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive },
- ]"
+ v-panel
+ :class="[$style.root, {
+ [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive,
+ [$style.large]: large,
+ }]"
>
- <ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
+ <ImgWithBlurhash
+ v-if="isThumbnailAvailable"
+ :hash="file.blurhash"
+ :src="file.thumbnailUrl"
+ :alt="file.name"
+ :title="file.name"
+ :cover="fit !== 'contain'"
+ :forceBlurhash="forceBlurhash"
+ />
<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
<i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i>
@@ -34,6 +42,8 @@ const props = defineProps<{
file: Misskey.entities.DriveFile;
fit: 'cover' | 'contain';
highlightWhenSensitive?: boolean;
+ forceBlurhash?: boolean;
+ large?: boolean;
}>();
const is = computed(() => {
@@ -60,7 +70,7 @@ const is = computed(() => {
const isThumbnailAvailable = computed(() => {
return props.file.thumbnailUrl
- ? (is.value === 'image' as const || is.value === 'video')
+ ? (is.value === 'image' || is.value === 'video')
: false;
});
</script>
@@ -101,4 +111,8 @@ const isThumbnailAvailable = computed(() => {
font-size: 32px;
color: #777;
}
+
+.large .icon {
+ font-size: 40px;
+}
</style>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index dc589a28e0..a782ae9d3b 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -186,11 +186,13 @@ function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): Cu
const parts = input.split('/').map(p => p.trim());
let currentNode: CustomEmojiFolderTree = root;
+ const currentPath = [];
for (const part of parts) {
+ currentPath.push(part);
let existingNode = currentNode.children.find((node) => node.value === part);
if (!existingNode) {
- const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
+ const newNode: CustomEmojiFolderTree = { value: part, category: currentPath.join("/"), children: [] };
currentNode.children.push(newNode);
existingNode = newNode;
}
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 0b4114d252..084c81bb52 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<KeepAlive>
<div v-show="opened">
- <MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22">
+ <MkSpacer v-if="withSpacer" :marginMin="spacerMin" :marginMax="spacerMax">
<slot></slot>
</MkSpacer>
<div v-else>
@@ -64,10 +64,14 @@ const props = withDefaults(defineProps<{
defaultOpen?: boolean;
maxHeight?: number | null;
withSpacer?: boolean;
+ spacerMin?: number;
+ spacerMax?: number;
}>(), {
defaultOpen: false,
maxHeight: null,
withSpacer: true,
+ spacerMin: 14,
+ spacerMax: 22,
});
const rootEl = shallowRef<HTMLElement>();
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index c7965aaac4..42e8485f46 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
class="_button"
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]"
- :disabled="wait"
+ :disabled="wait || disabled"
@click="onClick"
>
<template v-if="!wait">
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onBeforeUnmount, onMounted, ref } from 'vue';
+import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import * as os from '@/os.js';
@@ -51,13 +51,16 @@ const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed,
full?: boolean,
large?: boolean,
+ disabled?: boolean,
}>(), {
full: false,
large: false,
+ disabled: false,
});
const emit = defineEmits<{
- (_: 'update:user', value: Misskey.entities.UserDetailed): void
+ (_: 'update:user', value: Misskey.entities.UserDetailed): void,
+ (_: 'update:wait', value: boolean): void,
}>();
const isFollowing = ref(props.user.isFollowing);
@@ -65,6 +68,9 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
const wait = ref(false);
const connection = useStream().useChannel('main');
+// Emit the "wait" status so external components can synchronize state
+watch(wait, value => emit('update:wait', value));
+
if (props.user.isFollowing == null && $i) {
misskeyApi('users/show', {
userId: props.user.id,
diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue
index f409f6ce50..96214a9542 100644
--- a/packages/frontend/src/components/MkFormFooter.vue
+++ b/packages/frontend/src/components/MkFormFooter.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div>
<div style="margin-left: auto;" class="_buttons">
<MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton>
- <MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton primary rounded :disabled="!canSaving" @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</template>
@@ -18,7 +18,7 @@ import { } from 'vue';
import MkButton from './MkButton.vue';
import { i18n } from '@/i18n.js';
-const props = defineProps<{
+const props = withDefaults(defineProps<{
form: {
modifiedCount: {
value: number;
@@ -26,7 +26,10 @@ const props = defineProps<{
discard: () => void;
save: () => void;
};
-}>();
+ canSaving?: boolean;
+}>(), {
+ canSaving: true,
+});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index c04d0864fb..b0741aaf5e 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -111,8 +111,6 @@ function waitForDecode() {
.then(() => img.value?.decode())
.then(() => {
loaded.value = true;
- }, error => {
- console.log('Error occurred during decoding image', img.value, error);
});
} else {
loaded.value = false;
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 8ccbf61e48..d8066857fe 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.chart">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
- <optgroup :label="i18n.ts.federation">
+ <optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
<option value="federation">{{ i18n.ts._charts.federation }}</option>
<option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
</optgroup>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<optgroup :label="i18n.ts.notes">
<option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
<option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
- <option value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
+ <option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
<option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
</optgroup>
<optgroup :label="i18n.ts.drive">
@@ -46,9 +46,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
<option value="active-users">Active users</option>
<option value="notes">Notes</option>
- <option value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
- <option value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
- <option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
+ <option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
+ <option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
+ <option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
</MkSelect>
<div class="_panel" :class="$style.heatmap">
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
@@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFoldableSection>
- <MkFoldableSection class="item">
+ <MkFoldableSection v-if="shouldShowFederation" class="item">
<template #header>Federation</template>
<div :class="$style.federation">
<div class="pies">
@@ -84,13 +84,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, ref, shallowRef } from 'vue';
+import { onMounted, ref, computed, shallowRef } from 'vue';
import { Chart } from 'chart.js';
import MkSelect from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
+import { $i } from '@/account.js';
import * as os from '@/os.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import MkHeatmap, { type HeatmapSource } from '@/components/MkHeatmap.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
@@ -100,6 +102,8 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
+const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
+
const chartLimit = 500;
const chartSpan = ref<'hour' | 'day'>('hour');
const chartSrc = ref('active-users');
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index 2a8d5c9f71..9d9cc76822 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -4,19 +4,20 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="$style.root" :style="bg">
+<div :class="$style.root" :style="themeColorStyle">
<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/>
- <div :class="$style.name">{{ instance.name }}</div>
+ <div :class="$style.name">{{ instanceName }}</div>
</div>
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
-import { instanceName } from '@@/js/config.js';
-import { instance as Instance } from '@/instance.js';
+import { computed, type CSSProperties } from 'vue';
+import { instanceName as localInstanceName } from '@@/js/config.js';
+import { instance as localInstance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
+ host: string | null;
instance?: {
faviconUrl?: string | null
name?: string | null
@@ -25,18 +26,28 @@ const props = defineProps<{
}>();
// if no instance data is given, this is for the local instance
-const instance = props.instance ?? {
- name: instanceName,
- themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
-};
+const instanceName = computed(() => props.host == null ? localInstanceName : props.instance?.name ?? props.host);
-const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
-
-const themeColor = instance.themeColor ?? '#777777';
+const faviconUrl = computed(() => {
+ let imageSrc: string | null = null;
+ if (props.host == null) {
+ if (localInstance.iconUrl == null) {
+ return '/favicon.ico';
+ } else {
+ imageSrc = localInstance.iconUrl;
+ }
+ } else {
+ imageSrc = props.instance?.faviconUrl ?? null;
+ }
+ return getProxiedImageUrlNullable(imageSrc);
+});
-const bg = {
- background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
-};
+const themeColorStyle = computed<CSSProperties>(() => {
+ const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777';
+ return {
+ background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
+ };
+});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index f64ca4bc77..ac50d82a63 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { toUnicode } from 'punycode';
+import { toUnicode } from 'punycode.js';
import { computed } from 'vue';
import { host as localHost } from '@@/js/config.js';
import { $i } from '@/account.js';
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index c766a33823..a446dad0ab 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -288,20 +288,23 @@ const align = () => {
const onOpened = () => {
emit('opened');
- // NOTE: Chromatic テストの際に undefined になる場合がある
- if (content.value == null) return;
+ // contentの子要素にアクセスするためレンダリングの完了を待つ必要がある(nextTickが必要)
+ nextTick(() => {
+ // NOTE: Chromatic テストの際に undefined になる場合がある
+ if (content.value == null) return;
- // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
- const el = content.value.children[0];
- el.addEventListener('mousedown', ev => {
- contentClicking = true;
- window.addEventListener('mouseup', ev => {
- // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
- window.setTimeout(() => {
- contentClicking = false;
- }, 100);
- }, { passive: true, once: true });
- }, { passive: true });
+ // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
+ const el = content.value.children[0];
+ el.addEventListener('mousedown', ev => {
+ contentClicking = true;
+ window.addEventListener('mouseup', ev => {
+ // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
+ window.setTimeout(() => {
+ contentClicking = false;
+ }, 100);
+ }, { passive: true, once: true });
+ }, { passive: true });
+ });
};
const onClosed = () => {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 25d04a0b6a..3f52244bdc 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -56,13 +56,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
<MkNoteHeader :note="appearNote" :mini="true" @click.stop/>
- <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
+ <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
<bdi>
- <p v-if="appearNote.cw != null" :class="$style.cw">
+ <p v-if="mergedCW != null" :class="$style.cw">
<Mfm
- v-if="appearNote.cw != ''"
- :text="appearNote.cw"
+ v-if="mergedCW != ''"
+ :text="mergedCW"
:author="appearNote.user"
:nyaize="'respect'"
:enableEmojiMenu="true"
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
</p>
- <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
+ <div v-show="mergedCW == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
@@ -100,9 +100,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/>
</div>
- <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" @click.stop/>
+ <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
<div v-if="isEnabledUrlPreview">
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :class="$style.urlPreview" @click.stop/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/>
</div>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
@@ -128,11 +128,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
+ v-tooltip="renoteTooltip"
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop
- @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()"
+ @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
@@ -141,7 +142,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button
- v-if="canRenote && !props.mock"
+ v-if="canRenote && !props.mock && !$i?.rejectQuotes"
ref="quoteButton"
:class="$style.footerButton"
class="_button"
@@ -178,13 +179,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</template>
</I18n>
- <I18n v-else :src="i18n.ts.userSaysSomething" tag="small">
+ <I18n v-else-if="showSoftWordMutedWord !== true" :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
<MkUserName :user="appearNote.user"/>
</MkA>
</template>
</I18n>
+ <I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
+ <template #name>
+ <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
+ <MkUserName :user="appearNote.user"/>
+ </MkA>
+ </template>
+ <template #word>
+ {{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
+ </template>
+ </I18n>
</div>
<div v-else>
<!--
@@ -201,6 +212,7 @@ import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
import { shouldCollapsed } from '@@/js/collapsed.js';
import { host } from '@@/js/config.js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import type { MenuItem } from '@/types/menu.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
@@ -238,7 +250,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { useRouter } from '@/router/supplier.js';
-import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@@ -318,6 +330,7 @@ const isDeleted = ref(false);
const renoted = ref(false);
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
+const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord);
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
@@ -338,15 +351,24 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: `https://${host}/notes/${appearNote.value.id}`,
}));
+const mergedCW = computed(() => computeMergedCw(appearNote.value));
+
+const renoteTooltip = computeRenoteTooltip(renoted);
+
/* Overload FunctionにLintが対応していないのでコメントアウト
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
-function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): Array<string | string[]> | false | 'sensitiveMute';
*/
-function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
if (mutedWords != null) {
- if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
- if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
- if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
+ const result = checkWordMute(noteToCheck, $i, mutedWords);
+ if (Array.isArray(result)) return result;
+
+ const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
+ if (Array.isArray(replyResult)) return replyResult;
+
+ const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
+ if (Array.isArray(renoteResult)) return renoteResult;
}
if (checkOnly) return false;
@@ -506,10 +528,10 @@ if (!props.mock) {
}
}
-function boostVisibility() {
+function boostVisibility(forceMenu: boolean = false) {
if (renoting) return;
- if (!defaultStore.state.showVisibilitySelectorOnBoost) {
+ if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 243b95215a..1daca114c0 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -71,14 +71,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
</div>
</div>
- <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
+ <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
</div>
</header>
<div :class="$style.noteContent">
- <p v-if="appearNote.cw != null" :class="$style.cw">
+ <p v-if="mergedCW != null" :class="$style.cw">
<Mfm
- v-if="appearNote.cw != ''"
- :text="appearNote.cw"
+ v-if="mergedCW != ''"
+ :text="mergedCW"
:author="appearNote.user"
:nyaize="'respect'"
:enableEmojiMenu="true"
@@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
- <div v-show="appearNote.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
<Mfm
@@ -115,9 +115,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
</div>
- <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll"/>
+ <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
<div v-if="isEnabledUrlPreview">
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" style="margin-top: 6px;"/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/>
</div>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
</div>
@@ -140,10 +140,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
+ v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
- @mousedown.prevent="renoted ? undoRenote() : boostVisibility()"
+ @mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
@@ -152,7 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button
- v-if="canRenote"
+ v-if="canRenote && !$i?.rejectQuotes"
ref="quoteButton"
class="_button"
:class="$style.noteFooterButton"
@@ -244,6 +245,7 @@ import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
import { host } from '@@/js/config.js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -280,7 +282,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
-import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
import { type Keymap } from '@/scripts/hotkey.js';
@@ -347,6 +349,10 @@ const quotes = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
+const mergedCW = computed(() => computeMergedCw(appearNote.value));
+
+const renoteTooltip = computeRenoteTooltip(renoted);
+
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
@@ -478,10 +484,10 @@ useTooltip(quoteButton, async (showing) => {
});
});
-function boostVisibility() {
+function boostVisibility(forceMenu: boolean = false) {
if (renoting) return;
- if (!defaultStore.state.showVisibilitySelectorOnBoost) {
+ if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue
new file mode 100644
index 0000000000..bf105c3c27
--- /dev/null
+++ b/packages/frontend/src/components/MkNoteMediaGrid.vue
@@ -0,0 +1,109 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+ <template v-for="file in note.files">
+ <div
+ v-if="(((
+ (defaultStore.state.nsfw === 'force' || file.isSensitive) &&
+ defaultStore.state.nsfw !== 'ignore'
+ ) || (defaultStore.state.dataSaver.media && file.type.startsWith('image/'))) &&
+ !showingFiles.has(file.id)
+ )"
+ :class="[$style.filePreview, { [$style.square]: square }]"
+ @click="showingFiles.add(file.id)"
+ >
+ <MkDriveFileThumbnail
+ :file="file"
+ fit="cover"
+ :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia"
+ :forceBlurhash="true"
+ :large="true"
+ :class="$style.file"
+ />
+ <div :class="$style.sensitive">
+ <div>
+ <div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div>
+ <div v-else><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div>
+ <div>{{ i18n.ts.clickToShow }}</div>
+ </div>
+ </div>
+ </div>
+ <MkA v-else :class="[$style.filePreview, { [$style.square]: square }]" :to="notePage(note)">
+ <MkDriveFileThumbnail
+ :file="file"
+ fit="cover"
+ :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia"
+ :large="true"
+ :class="$style.file"
+ />
+ </MkA>
+ </template>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import { notePage } from '@/filters/note.js';
+import { i18n } from '@/i18n.js';
+import * as Misskey from 'misskey-js';
+import { defaultStore } from '@/store.js';
+import bytes from '@/filters/bytes.js';
+
+import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
+
+defineProps<{
+ note: Misskey.entities.Note;
+ square?: boolean;
+}>();
+
+const showingFiles = ref<Set<string>>(new Set());
+</script>
+
+<style lang="scss" module>
+.square {
+ width: 100%;
+ height: auto;
+ aspect-ratio: 1;
+}
+
+.filePreview {
+ position: relative;
+ height: 128px;
+ border-radius: calc(var(--MI-radius) / 2);
+ overflow: clip;
+
+ &:hover {
+ text-decoration: none;
+ }
+
+ &.square {
+ height: 100%;
+ }
+}
+
+.file {
+ width: 100%;
+ height: 100%;
+ border-radius: calc(var(--MI-radius) / 2);
+}
+
+.sensitive {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: grid;
+ place-items: center;
+ font-size: 0.8em;
+ text-align: center;
+ padding: 8px;
+ box-sizing: border-box;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(5px);
+ cursor: pointer;
+}
+</style>
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue
index 9ef45a0ec3..3720aa7493 100644
--- a/packages/frontend/src/components/MkNoteSimple.vue
+++ b/packages/frontend/src/components/MkNoteSimple.vue
@@ -9,11 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div>
- <p v-if="note.cw != null" :class="$style.cw">
- <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :isBlock="true" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
+ <p v-if="mergedCW != null" :class="$style.cw">
+ <Mfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :isBlock="true" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
<MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/>
</p>
- <div v-show="note.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note" :expandAllCws="props.expandAllCws"/>
<div v-if="note.isSchedule" style="margin-top: 10px;">
<MkButton :class="$style.button" inline @click.stop.prevent="editScheduleNote()"><i class="ti ti-eraser"></i> {{ i18n.ts.edit }}</MkButton>
@@ -26,8 +26,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import * as os from '@/os.js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
@@ -48,6 +49,8 @@ const props = defineProps<{
let showContent = ref(defaultStore.state.uncollapseCW);
const isDeleted = ref(false);
+const mergedCW = computed(() => computeMergedCw(props.note));
+
const emit = defineEmits<{
(ev: 'editScheduleNote'): void;
}>();
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 3523babe46..b2967a7cf3 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -11,11 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.body">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div :class="$style.content">
- <p v-if="note.cw != null" :class="$style.cw">
- <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :isBlock="true" :author="note.user" :nyaize="'respect'"/>
+ <p v-if="mergedCW != null" :class="$style.cw">
+ <Mfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :isBlock="true" :author="note.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
</p>
- <div v-show="note.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation" :expandAllCws="props.expandAllCws"/>
</div>
</div>
@@ -28,16 +28,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
+ v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
- @mousedown="renoted ? undoRenote() : boostVisibility()"
+ @mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
</button>
<button
- v-if="canRenote"
+ v-if="canRenote && !$i?.rejectQuotes"
ref="quoteButton"
class="_button"
:class="$style.noteFooterButton"
@@ -85,6 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
@@ -106,7 +108,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
-import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
@@ -135,10 +137,14 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
+const renoteTooltip = computeRenoteTooltip(computed);
+
let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const replies = ref<Misskey.entities.Note[]>([]);
+const mergedCW = computed(() => computeMergedCw(appearNote.value));
+
const isRenote = (
props.note.renote != null &&
props.note.text == null &&
@@ -285,8 +291,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
-function boostVisibility() {
- if (!defaultStore.state.showVisibilitySelectorOnBoost) {
+function boostVisibility(forceMenu: boolean = false) {
+ if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index b13df2813b..bd157d0b14 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, shallowRef, ref } from 'vue';
+import { defineAsyncComponent, shallowRef } from 'vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 4620b966af..a910151e42 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -122,6 +122,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
+ <div v-if="full" :class="$style.followRequestCommands">
+ <MkFollowButton v-if="userDetailed" :class="$style.followCommandButton" :user="userDetailed" :transparent="false" :full="false"/>
+ </div>
</template>
<template v-else-if="notification.type === 'followRequestAccepted'">
<div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div>
@@ -136,6 +139,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
+ <MkFollowButton v-if="userDetailed" :class="$style.followCommandButton" :user="userDetailed" :transparent="false" :full="false"/>
</div>
</template>
<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>
@@ -179,8 +183,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { Ref, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
+import { UserDetailed } from 'misskey-js/autogen/models.js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
@@ -190,6 +195,7 @@ import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { signinRequired } from '@/account.js';
import { infoImageUrl } from '@/instance.js';
+import MkFollowButton from '@/components/MkFollowButton.vue';
const $i = signinRequired();
@@ -202,6 +208,28 @@ const props = withDefaults(defineProps<{
full: false,
});
+const userDetailed: Ref<UserDetailed | null> = ref(null);
+
+const followRequestDone = ref(true);
+
+// watch() is required because computed() doesn't support async.
+watch(props, async () => {
+ const type = props.notification.type;
+
+ // To avoid extra lookups, only do the query when it actually matters.
+ if (type === 'follow' || type === 'receiveFollowRequest') {
+ const user = await misskeyApi('users/show', {
+ userId: props.notification.userId,
+ });
+
+ userDetailed.value = user;
+ followRequestDone.value = !user.hasPendingFollowRequestToYou;
+ } else {
+ userDetailed.value = null;
+ followRequestDone.value = false;
+ }
+}, { immediate: true });
+
type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' };
const exportEntityName = {
@@ -216,8 +244,6 @@ const exportEntityName = {
userList: i18n.ts.lists,
} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>;
-const followRequestDone = ref(false);
-
const acceptFollowRequest = () => {
if (!('user' in props.notification)) return;
followRequestDone.value = true;
@@ -434,13 +460,24 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
.followRequestCommands {
display: flex;
gap: 8px;
- max-width: 300px;
margin-top: 8px;
+ width: 100%;
}
.followRequestCommandButton {
+ max-width: 175px;
+ width: 100%;
+}
+
+.flexSpacer {
flex: 1;
}
+.followCommandButton {
+ margin-left: auto;
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
.reactionsItem {
display: inline-block;
position: relative;
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 84189211b6..3ff4cc215c 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
import { url } from '@@/js/config.js';
import { getScrollContainer } from '@@/js/scroll.js';
+import MkUserName from './global/MkUserName.vue';
import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js';
@@ -43,7 +44,6 @@ import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { useRouterFactory } from '@/router/supplier.js';
import { mainRouter } from '@/router/main.js';
-import MkUserName from './global/MkUserName.vue';
const props = defineProps<{
initialPath: string;
diff --git a/packages/frontend/src/components/MkPagingButtons.vue b/packages/frontend/src/components/MkPagingButtons.vue
new file mode 100644
index 0000000000..fe59efd83a
--- /dev/null
+++ b/packages/frontend/src/components/MkPagingButtons.vue
@@ -0,0 +1,124 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+ <MkButton primary :disabled="min === current" @click="onToPrevButtonClicked">&lt;</MkButton>
+
+ <div :class="$style.buttons">
+ <div v-if="prevDotVisible" :class="$style.headTailButtons">
+ <MkButton @click="onToHeadButtonClicked">{{ min }}</MkButton>
+ <span class="ti ti-dots"/>
+ </div>
+
+ <MkButton
+ v-for="i in buttonRanges" :key="i"
+ :disabled="current === i"
+ @click="onNumberButtonClicked(i)"
+ >
+ {{ i }}
+ </MkButton>
+
+ <div v-if="nextDotVisible" :class="$style.headTailButtons">
+ <span class="ti ti-dots"/>
+ <MkButton @click="onToTailButtonClicked">{{ max }}</MkButton>
+ </div>
+ </div>
+
+ <MkButton primary :disabled="max === current" @click="onToNextButtonClicked">&gt;</MkButton>
+</div>
+</template>
+
+<script setup lang="ts">
+
+import { computed, toRefs } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+
+const min = 1;
+
+const emit = defineEmits<{
+ (ev: 'pageChanged', pageNumber: number): void;
+}>();
+
+const props = defineProps<{
+ current: number;
+ max: number;
+ buttonCount: number;
+}>();
+
+const { current, max } = toRefs(props);
+
+const buttonCount = computed(() => Math.min(max.value, props.buttonCount));
+const buttonCountHalf = computed(() => Math.floor(buttonCount.value / 2));
+const buttonCountStart = computed(() => Math.min(Math.max(min, current.value - buttonCountHalf.value), max.value - buttonCount.value + 1));
+const buttonRanges = computed(() => Array.from({ length: buttonCount.value }, (_, i) => buttonCountStart.value + i));
+
+const prevDotVisible = computed(() => (current.value - 1 > buttonCountHalf.value) && (max.value > buttonCount.value));
+const nextDotVisible = computed(() => (current.value < max.value - buttonCountHalf.value) && (max.value > buttonCount.value));
+
+if (_DEV_) {
+ console.log('[MkPagingButtons]', current.value, max.value, buttonCount.value, buttonCountHalf.value);
+ console.log('[MkPagingButtons]', current.value < max.value - buttonCountHalf.value);
+ console.log('[MkPagingButtons]', max.value > buttonCount.value);
+}
+
+function onNumberButtonClicked(pageNumber: number) {
+ emit('pageChanged', pageNumber);
+}
+
+function onToHeadButtonClicked() {
+ emit('pageChanged', min);
+}
+
+function onToPrevButtonClicked() {
+ const newPageNumber = current.value <= min ? min : current.value - 1;
+ emit('pageChanged', newPageNumber);
+}
+
+function onToNextButtonClicked() {
+ const newPageNumber = current.value >= max.value ? max.value : current.value + 1;
+ emit('pageChanged', newPageNumber);
+}
+
+function onToTailButtonClicked() {
+ emit('pageChanged', max.value);
+}
+</script>
+
+<style module lang="scss">
+.root {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 24px;
+
+ button {
+ border-radius: 9999px;
+ min-width: 2.5em;
+ min-height: 2.5em;
+ max-width: 2.5em;
+ max-height: 2.5em;
+ padding: 4px;
+ }
+}
+
+.buttons {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+.headTailButtons {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+
+ span {
+ font-size: 0.75em;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index a414676bda..f6218de4c8 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span :class="$style.fg">
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
- <Mfm :text="choice.text" :plain="true"/>
+ <Mfm :text="choice.text" :plain="true" :author="author" :emojiUrls="emojiUrls"/>
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
</span>
</li>
@@ -48,6 +48,8 @@ const props = defineProps<{
poll: NonNullable<Misskey.entities.Note['poll']>;
readOnly?: boolean;
local?: boolean;
+ emojiUrls?: Record<string, string>;
+ author?: Misskey.entities.UserLite;
}>();
const remaining = ref(-1);
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 11ae6dbd6a..ca227d649a 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -46,14 +46,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template>
<template v-else>{{ submitText }}</template>
- <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i>
+ <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renoteTargetNote ? 'ti ti-quote' : 'ti ti-send'"></i>
</div>
</button>
</div>
</header>
<MkNoteSimple v-if="reply" :class="$style.targetNote" :hideFiles="true" :note="reply"/>
- <MkNoteSimple v-if="renote" :class="$style.targetNote" :hideFiles="true" :note="renote"/>
- <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
+ <MkNoteSimple v-if="renoteTargetNote" :class="$style.targetNote" :hideFiles="true" :note="renoteTargetNote"/>
+ <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null; renoteTargetNote = null;"><i class="ti ti-x"></i></button></div>
<div v-if="visibility === 'specified'" :class="$style.toSpecified">
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
<div :class="$style.visibleUsers">
@@ -106,13 +106,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, toRaw } from 'vue';
+import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, toRaw, type ShallowRef } from 'vue';
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
-import { toASCII } from 'punycode/';
+import { toASCII } from 'punycode.js';
import { host, url } from '@@/js/config.js';
+import { appendContentWarning } from '@@/js/append-content-warning.js';
import type { MenuItem } from '@/types/menu.js';
+import type { PostFormProps } from '@/types/post-form.js';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
@@ -136,7 +138,6 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
-import type { PostFormProps } from '@/types/post-form.js';
import MkScheduleEditor from '@/components/MkScheduleEditor.vue';
const $i = signinRequired();
@@ -202,12 +203,13 @@ const justEndedComposition = ref(false);
const scheduleNote = ref<{
scheduledAt: number | null;
} | null>(null);
+const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
- if (props.renote) {
- key += `renote:${props.renote.id}`;
+ if (renoteTargetNote.value) {
+ key += `renote:${renoteTargetNote.value.id}`;
} else if (props.reply) {
key += `reply:${props.reply.id}`;
} else {
@@ -218,7 +220,7 @@ const draftKey = computed((): string => {
});
const placeholder = computed((): string => {
- if (props.renote) {
+ if (renoteTargetNote.value) {
return i18n.ts._postForm.quotePlaceholder;
} else if (props.reply) {
return i18n.ts._postForm.replyPlaceholder;
@@ -238,7 +240,7 @@ const placeholder = computed((): string => {
});
const submitText = computed((): string => {
- return props.renote
+ return renoteTargetNote.value
? i18n.ts.quote
: props.reply
? i18n.ts.reply
@@ -262,11 +264,12 @@ const canPost = computed((): boolean => {
1 <= textLength.value ||
1 <= files.value.length ||
poll.value != null ||
- props.renote != null ||
+ renoteTargetNote.value != null ||
quoteId.value != null
) &&
(textLength.value <= maxTextLength.value) &&
(cwLength.value <= maxCwLength.value) &&
+ (files.value.length <= 16) &&
(!poll.value || poll.value.choices.length >= 2);
});
@@ -364,6 +367,19 @@ if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
cw.value = props.reply.cw;
}
+// apply default CW
+if ($i.defaultCW) {
+ useCw.value = true;
+
+ if (!cw.value || $i.defaultCWPriority === 'default') {
+ cw.value = $i.defaultCW;
+ } else if ($i.defaultCWPriority !== 'parent') {
+ const putDefaultFirst = $i.defaultCWPriority === 'defaultParent';
+ cw.value = appendContentWarning(cw.value, $i.defaultCW, putDefaultFirst);
+ }
+ // else { do nothing, because existing CW takes priority. }
+}
+
function watchForDraft() {
watch(text, () => saveDraft());
watch(useCw, () => saveDraft());
@@ -624,7 +640,7 @@ async function onPaste(ev: ClipboardEvent) {
const paste = ev.clipboardData.getData('text');
- if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) {
+ if (!renoteTargetNote.value && !quoteId.value && paste.startsWith(url + '/notes/') && !$i.rejectQuotes) {
ev.preventDefault();
os.confirm({
@@ -840,7 +856,7 @@ async function post(ev?: MouseEvent) {
text: text.value === '' ? null : text.value,
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined,
- renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
+ renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : undefined,
channelId: props.channel ? props.channel.id : undefined,
poll: poll.value,
cw: useCw.value ? cw.value ?? '' : null,
@@ -930,7 +946,7 @@ async function post(ev?: MouseEvent) {
claimAchievement('brainDiver');
}
- if (props.renote && (props.renote.userId === $i.id) && text.length > 0) {
+ if (renoteTargetNote.value && (renoteTargetNote.value.userId === $i.id) && text.length > 0) {
claimAchievement('selfQuote');
}
@@ -1140,7 +1156,7 @@ onMounted(() => {
users.forEach(u => pushVisibleUser(u));
});
}
- quoteId.value = init.renote ? init.renote.id : null;
+ quoteId.value = renoteTargetNote.value ? renoteTargetNote.value.id : null;
reactionAcceptance.value = init.reactionAcceptance;
if (init.isSchedule) {
scheduleNote.value = {
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 11444d8d78..bab7d22112 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -22,7 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
</Sortable>
- <p :class="$style.remain">{{ 16 - props.modelValue.length }}/16</p>
+ <p :class="[$style.remain, {
+ [$style.exceeded]: props.modelValue.length > 16,
+ }]">{{ 16 - props.modelValue.length }}/16</p>
</div>
</template>
@@ -239,5 +241,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
margin: 0;
padding: 0;
font-size: 90%;
+
+ &.exceeded {
+ color: var(--MI_THEME-error);
+ }
}
</style>
diff --git a/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue
new file mode 100644
index 0000000000..873b276b3d
--- /dev/null
+++ b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue
@@ -0,0 +1,132 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkWindow
+ ref="windowEl"
+ :initialWidth="400"
+ :initialHeight="500"
+ :canResize="true"
+ @close="windowEl?.close()"
+ @closed="emit('closed')"
+>
+ <template #header>:{{ name }}:</template>
+
+ <div style="display: flex; flex-direction: column; min-height: 100%;">
+ <MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
+ <div class="_gaps_m">
+ <div v-if="imgUrl != null" :class="$style.imgs">
+ <div style="background: #000;" :class="$style.imgContainer">
+ <img :src="imgUrl" :class="$style.img" :alt="name"/>
+ </div>
+ <div style="background: #222;" :class="$style.imgContainer">
+ <img :src="imgUrl" :class="$style.img" :alt="name"/>
+ </div>
+ <div style="background: #ddd;" :class="$style.imgContainer">
+ <img :src="imgUrl" :class="$style.img" :alt="name"/>
+ </div>
+ <div style="background: #fff;" :class="$style.imgContainer">
+ <img :src="imgUrl" :class="$style.img" :alt="name"/>
+ </div>
+ </div>
+
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.id }}</template>
+ <template #value>{{ name }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.host }}</template>
+ <template #value>{{ host }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.license }}</template>
+ <template #value>{{ license }}</template>
+ </MkKeyValue>
+ </div>
+ </MkSpacer>
+ <div :class="$style.footer">
+ <MkButton primary rounded style="margin: 0 auto;" @click="done">
+ <i class="ti ti-plus"></i> {{ i18n.ts.import }}
+ </MkButton>
+ </div>
+ </div>
+</MkWindow>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import MkWindow from '@/components/MkWindow.vue';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+const props = defineProps<{
+ emoji: {
+ id: string,
+ name: string,
+ host: string,
+ license: string | null,
+ url: string
+ },
+}>();
+
+const emit = defineEmits<{
+ // 必要なら戻り値を増やす
+ (ev: 'done'): void,
+ (ev: 'closed'): void
+}>();
+
+const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
+
+const name = computed(() => props.emoji.name);
+const host = computed(() => props.emoji.host);
+const license = computed(() => props.emoji.license);
+const imgUrl = computed(() => props.emoji.url);
+
+async function done() {
+ await os.apiWithDialog('admin/emoji/copy', {
+ emojiId: props.emoji.id,
+ });
+
+ emit('done');
+ windowEl.value?.close();
+}
+</script>
+
+<style lang="scss" module>
+.imgs {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.imgContainer {
+ padding: 8px;
+ border-radius: 6px;
+}
+
+.img {
+ display: block;
+ height: 64px;
+ width: 64px;
+ object-fit: contain;
+}
+
+.footer {
+ position: sticky;
+ z-index: 10000;
+ bottom: 0;
+ left: 0;
+ padding: 12px;
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ background: var(--MI_THEME-acrylicBg);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+}
+</style>
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts b/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts
new file mode 100644
index 0000000000..411d62edf9
--- /dev/null
+++ b/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts
@@ -0,0 +1,106 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import { http, HttpResponse } from 'msw';
+import { role } from '../../.storybook/fakes.js';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import MkRoleSelectDialog from '@/components/MkRoleSelectDialog.vue';
+
+const roles = [
+ role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'),
+ role({ displayOrder: 2 }, '2'), role({ displayOrder: 2 }, '2'), role({ displayOrder: 3 }, '3'), role({ displayOrder: 3 }, '3'),
+ role({ displayOrder: 4 }, '4'), role({ displayOrder: 5 }, '5'), role({ displayOrder: 6 }, '6'), role({ displayOrder: 7 }, '7'),
+ role({ displayOrder: 999, name: 'privateRole', isPublic: false }, '999'),
+];
+
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkRoleSelectDialog,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkRoleSelectDialog v-bind="props" />',
+ };
+ },
+ args: {
+ initialRoleIds: undefined,
+ infoMessage: undefined,
+ title: undefined,
+ publicOnly: true,
+ },
+ parameters: {
+ layout: 'centered',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ http.post('/api/admin/roles/list', ({ params }) => {
+ return HttpResponse.json(roles);
+ }),
+ ],
+ },
+ },
+ decorators: [() => ({
+ template: '<div style="width:100cqmin"><story/></div>',
+ })],
+} satisfies StoryObj<typeof MkRoleSelectDialog>;
+
+export const InitialIds = {
+ ...Default,
+ args: {
+ ...Default.args,
+ initialRoleIds: [roles[0].id, roles[1].id, roles[4].id, roles[6].id, roles[8].id, roles[10].id],
+ },
+} satisfies StoryObj<typeof MkRoleSelectDialog>;
+
+export const InfoMessage = {
+ ...Default,
+ args: {
+ ...Default.args,
+ infoMessage: 'This is a message.',
+ },
+} satisfies StoryObj<typeof MkRoleSelectDialog>;
+
+export const Title = {
+ ...Default,
+ args: {
+ ...Default.args,
+ title: 'Select roles',
+ },
+} satisfies StoryObj<typeof MkRoleSelectDialog>;
+
+export const Full = {
+ ...Default,
+ args: {
+ ...Default.args,
+ initialRoleIds: roles.map(it => it.id),
+ infoMessage: InfoMessage.args.infoMessage,
+ title: Title.args.title,
+ },
+} satisfies StoryObj<typeof MkRoleSelectDialog>;
+
+export const FullWithPrivate = {
+ ...Default,
+ args: {
+ ...Default.args,
+ initialRoleIds: roles.map(it => it.id),
+ infoMessage: InfoMessage.args.infoMessage,
+ title: Title.args.title,
+ publicOnly: false,
+ },
+} satisfies StoryObj<typeof MkRoleSelectDialog>;
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue
new file mode 100644
index 0000000000..8d11bd855f
--- /dev/null
+++ b/packages/frontend/src/components/MkRoleSelectDialog.vue
@@ -0,0 +1,200 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+ ref="windowEl"
+ :withOkButton="false"
+ :okButtonDisabled="false"
+ :width="400"
+ :height="500"
+ @close="onCloseModalWindow"
+ @closed="$emit('dispose')"
+>
+ <template #header>{{ title }}</template>
+ <MkSpacer :marginMin="20" :marginMax="28">
+ <MkLoading v-if="fetching"/>
+ <div v-else class="_gaps" :class="$style.root">
+ <div :class="$style.header">
+ <MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+ </div>
+
+ <div v-if="selectedRoles.length > 0" class="_gaps" :class="$style.roleItemArea">
+ <div v-for="role in selectedRoles" :key="role.id" :class="$style.roleItem">
+ <MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
+ <button class="_button" :class="$style.roleUnAssign" @click="removeRole(role.id)"><i class="ti ti-x"></i></button>
+ </div>
+ </div>
+ <div v-else :class="$style.roleItemArea" style="text-align: center">
+ {{ i18n.ts._roleSelectDialog.notSelected }}
+ </div>
+
+ <MkInfo v-if="infoMessage">{{ infoMessage }}</MkInfo>
+
+ <div :class="$style.buttons">
+ <MkButton primary @click="onOkClicked">{{ i18n.ts.ok }}</MkButton>
+ <MkButton @click="onCancelClicked">{{ i18n.ts.cancel }}</MkButton>
+ </div>
+ </div>
+ </MkSpacer>
+</MkModalWindow>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, toRefs } from 'vue';
+import * as Misskey from 'misskey-js';
+import { i18n } from '@/i18n.js';
+import MkButton from '@/components/MkButton.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkRolePreview from '@/components/MkRolePreview.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import * as os from '@/os.js';
+import MkSpacer from '@/components/global/MkSpacer.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkLoading from '@/components/global/MkLoading.vue';
+
+const emit = defineEmits<{
+ (ev: 'done', value: Misskey.entities.Role[]),
+ (ev: 'close'),
+ (ev: 'dispose'),
+}>();
+
+const props = withDefaults(defineProps<{
+ initialRoleIds?: string[],
+ infoMessage?: string,
+ title?: string,
+ publicOnly: boolean,
+}>(), {
+ initialRoleIds: undefined,
+ infoMessage: undefined,
+ title: undefined,
+ publicOnly: true,
+});
+
+const { initialRoleIds, infoMessage, title, publicOnly } = toRefs(props);
+
+const windowEl = ref<InstanceType<typeof MkModalWindow>>();
+const roles = ref<Misskey.entities.Role[]>([]);
+const selectedRoleIds = ref<string[]>(initialRoleIds.value ?? []);
+const fetching = ref(false);
+
+const selectedRoles = computed(() => {
+ const r = roles.value.filter(role => selectedRoleIds.value.includes(role.id));
+ r.sort((a, b) => {
+ if (a.displayOrder !== b.displayOrder) {
+ return b.displayOrder - a.displayOrder;
+ }
+
+ return a.id.localeCompare(b.id);
+ });
+ return r;
+});
+
+async function fetchRoles() {
+ fetching.value = true;
+ const result = await misskeyApi('admin/roles/list', {});
+ roles.value = result.filter(it => publicOnly.value ? it.isPublic : true);
+ fetching.value = false;
+}
+
+async function addRole() {
+ const items = roles.value
+ .filter(r => r.isPublic)
+ .filter(r => !selectedRoleIds.value.includes(r.id))
+ .map(r => ({ text: r.name, value: r }));
+
+ const { canceled, result: role } = await os.select({ items });
+ if (canceled) {
+ return;
+ }
+
+ selectedRoleIds.value.push(role.id);
+}
+
+async function removeRole(roleId: string) {
+ selectedRoleIds.value = selectedRoleIds.value.filter(x => x !== roleId);
+}
+
+function onOkClicked() {
+ emit('done', selectedRoles.value);
+ windowEl.value?.close();
+}
+
+function onCancelClicked() {
+ emit('close');
+ windowEl.value?.close();
+}
+
+function onCloseModalWindow() {
+ emit('close');
+ windowEl.value?.close();
+}
+
+fetchRoles();
+</script>
+
+<style module lang="scss">
+.root {
+ max-height: 410px;
+ height: 410px;
+ display: flex;
+ flex-direction: column;
+}
+
+.roleItemArea {
+ background-color: var(--MI_THEME-acrylicBg);
+ border-radius: var(--MI-radius);
+ padding: 12px;
+ overflow-y: auto;
+}
+
+.roleItem {
+ display: flex;
+}
+
+.role {
+ flex: 1;
+}
+
+.roleUnAssign {
+ width: 32px;
+ height: 32px;
+ margin-left: 8px;
+ align-self: center;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.title {
+ flex: 1;
+}
+
+.addRoleButton {
+ min-width: 32px;
+ min-height: 32px;
+ max-width: 32px;
+ max-height: 32px;
+ margin-left: 8px;
+ align-self: center;
+ padding: 0;
+}
+
+.buttons {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ margin-top: auto;
+}
+
+.divider {
+ border-top: solid 0.5px var(--MI_THEME-divider);
+}
+
+</style>
diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue
index 34c22abc31..e98ac9cfd2 100644
--- a/packages/frontend/src/components/MkSignin.input.vue
+++ b/packages/frontend/src/components/MkSignin.input.vue
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { ref } from 'vue';
-import { toUnicode } from 'punycode/';
+import { toUnicode } from 'punycode.js';
import { query, extractDomain } from '@@/js/url.js';
import { host as configHost } from '@@/js/config.js';
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 4a6219071b..d6177762d2 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -141,6 +141,7 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
return;
}
emit('login', res.signinResponse);
+ onLoginSucceeded(res.signinResponse);
}).catch(onSigninApiError);
} else if (userInfo.value != null) {
tryLogin({
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index e636712389..dd263ce642 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.invitationCode }}</template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
- <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername">
+ <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username debounce @update:modelValue="onChangeUsername">
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
@@ -85,7 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
-import { toUnicode } from 'punycode/';
+import { toUnicode } from 'punycode.js';
import * as Misskey from 'misskey-js';
import * as config from '@@/js/config.js';
import MkButton from './MkButton.vue';
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 06481b808c..d1685c6990 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -10,8 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
- <div v-if="instance.disableRegistration">
- <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
+ <div v-if="instance.disableRegistration || instance.federation !== 'all'" class="_gaps_s">
+ <MkInfo v-if="instance.disableRegistration" warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
+ <MkInfo v-if="instance.federation === 'specified'" warn>{{ i18n.ts.federationSpecified }}</MkInfo>
+ <MkInfo v-else-if="instance.federation === 'none'" warn>{{ i18n.ts.federationDisabled }}</MkInfo>
</div>
<div style="text-align: center;">
diff --git a/packages/frontend/src/components/MkSortOrderEditor.define.ts b/packages/frontend/src/components/MkSortOrderEditor.define.ts
new file mode 100644
index 0000000000..f023b5d72b
--- /dev/null
+++ b/packages/frontend/src/components/MkSortOrderEditor.define.ts
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type SortOrderDirection = '+' | '-'
+
+export type SortOrder<T extends string> = {
+ key: T;
+ direction: SortOrderDirection;
+}
diff --git a/packages/frontend/src/components/MkSortOrderEditor.vue b/packages/frontend/src/components/MkSortOrderEditor.vue
new file mode 100644
index 0000000000..9decacc5f5
--- /dev/null
+++ b/packages/frontend/src/components/MkSortOrderEditor.vue
@@ -0,0 +1,118 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.sortOrderArea">
+ <div :class="$style.sortOrderAreaTags">
+ <MkTagItem
+ v-for="order in currentOrders"
+ :key="order.key"
+ :iconClass="order.direction === '+' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'"
+ :exButtonIconClass="'ti ti-x'"
+ :content="order.key"
+ :class="$style.sortOrderTag"
+ @click="onToggleSortOrderButtonClicked(order)"
+ @exButtonClick="onRemoveSortOrderButtonClicked(order)"
+ />
+ </div>
+ <MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked">
+ <span class="ti ti-plus"></span>
+ </MkButton>
+</div>
+</template>
+
+<script setup lang="ts" generic="T extends string">
+import { toRefs } from 'vue';
+import MkTagItem from '@/components/MkTagItem.vue';
+import MkButton from '@/components/MkButton.vue';
+import { MenuItem } from '@/types/menu.js';
+import * as os from '@/os.js';
+import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
+
+const emit = defineEmits<{
+ (ev: 'update', sortOrders: SortOrder<T>[]): void;
+}>();
+
+const props = defineProps<{
+ baseOrderKeyNames: T[];
+ currentOrders: SortOrder<T>[];
+}>();
+
+const { currentOrders } = toRefs(props);
+
+function onToggleSortOrderButtonClicked(order: SortOrder<T>) {
+ switch (order.direction) {
+ case '+':
+ order.direction = '-';
+ break;
+ case '-':
+ order.direction = '+';
+ break;
+ }
+
+ emitOrder(currentOrders.value);
+}
+
+function onAddSortOrderButtonClicked(ev: MouseEvent) {
+ const menuItems: MenuItem[] = props.baseOrderKeyNames
+ .filter(baseKey => !currentOrders.value.map(it => it.key).includes(baseKey))
+ .map(it => {
+ return {
+ text: it,
+ action: () => {
+ emitOrder([...currentOrders.value, { key: it, direction: '+' }]);
+ },
+ };
+ });
+ os.contextMenu(menuItems, ev);
+}
+
+function onRemoveSortOrderButtonClicked(order: SortOrder<T>) {
+ emitOrder(currentOrders.value.filter(it => it.key !== order.key));
+}
+
+function emitOrder(sortOrders: SortOrder<T>[]) {
+ emit('update', sortOrders);
+}
+
+</script>
+
+<style module lang="scss">
+.sortOrderArea {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: flex-start;
+}
+
+.sortOrderAreaTags {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.sortOrderAddButton {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ box-sizing: border-box;
+ min-width: 2.0em;
+ min-height: 2.0em;
+ max-width: 2.0em;
+ max-height: 2.0em;
+ padding: 8px;
+ margin-left: auto;
+ border-radius: 9999px;
+ background-color: var(--MI_THEME-buttonBg);
+}
+
+.sortOrderTag {
+ user-select: none;
+ cursor: pointer;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSparkle.vue b/packages/frontend/src/components/MkSparkle.vue
index 8491ce2f84..b3fc67c0df 100644
--- a/packages/frontend/src/components/MkSparkle.vue
+++ b/packages/frontend/src/components/MkSparkle.vue
@@ -39,32 +39,18 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<!-- MFMで上位レイヤーに表示されるため、リンクをクリックできるようにstyleにpointer-events: none;を付与。 -->
<svg v-for="particle in particles" :key="particle.id" :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg" style="position: absolute; top: -32px; left: -32px; pointer-events: none;">
+ <!-- SVGのanimateTransformを使用するとChromeで描画できなくなるためCSSアニメーションを使用している (Issue 14155) -->
<path
- style="transform-origin: center; transform-box: fill-box;"
- :transform="`translate(${particle.x} ${particle.y})`"
+ :style="{
+ '--translateX': particle.x + 'px',
+ '--translateY': particle.y + 'px',
+ '--duration': particle.dur + 'ms',
+ '--size': particle.size,
+ }"
+ :class="$style.particle"
:fill="particle.color"
d="M29.427,2.011C29.721,0.83 30.782,0 32,0C33.218,0 34.279,0.83 34.573,2.011L39.455,21.646C39.629,22.347 39.991,22.987 40.502,23.498C41.013,24.009 41.653,24.371 42.354,24.545L61.989,29.427C63.17,29.721 64,30.782 64,32C64,33.218 63.17,34.279 61.989,34.573L42.354,39.455C41.653,39.629 41.013,39.991 40.502,40.502C39.991,41.013 39.629,41.653 39.455,42.354L34.573,61.989C34.279,63.17 33.218,64 32,64C30.782,64 29.721,63.17 29.427,61.989L24.545,42.354C24.371,41.653 24.009,41.013 23.498,40.502C22.987,39.991 22.347,39.629 21.646,39.455L2.011,34.573C0.83,34.279 0,33.218 0,32C0,30.782 0.83,29.721 2.011,29.427L21.646,24.545C22.347,24.371 22.987,24.009 23.498,23.498C24.009,22.987 24.371,22.347 24.545,21.646L29.427,2.011Z"
- >
- <animateTransform
- attributeName="transform"
- attributeType="XML"
- type="rotate"
- from="0 0 0"
- to="360 0 0"
- :dur="`${particle.dur}ms`"
- repeatCount="1"
- additive="sum"
- />
- <animateTransform
- attributeName="transform"
- attributeType="XML"
- type="scale"
- :values="`0; ${particle.size}; 0`"
- :dur="`${particle.dur}ms`"
- repeatCount="1"
- additive="sum"
- />
- </path>
+ ></path>
</svg>
</span>
</template>
@@ -130,4 +116,25 @@ onUnmounted(() => {
position: relative;
display: inline-block;
}
+
+.particle {
+ transform-origin: center;
+ transform-box: fill-box;
+ translate: var(--translateX) var(--translateY);
+ animation: particleAnimation var(--duration) linear infinite;
+}
+
+@keyframes particleAnimation {
+ 0% {
+ rotate: 0deg;
+ scale: 0;
+ }
+ 50% {
+ scale: var(--size);
+ }
+ 100% {
+ rotate: 360deg;
+ scale: 0;
+ }
+}
</style>
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index a32fd53c51..145de3b9d3 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</details>
<details v-if="note.poll">
<summary>{{ i18n.ts.poll }}</summary>
- <MkPoll :noteId="note.id" :poll="note.poll"/>
+ <MkPoll :noteId="note.id" :poll="note.poll" :author="note.user" :emojiUrls="note.emojis"/>
</details>
<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click.stop="collapsed = false">
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
@@ -42,11 +42,11 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed, watch } from 'vue';
import * as Misskey from 'misskey-js';
import * as mfm from '@transfem-org/sfm-js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
-import { shouldCollapsed } from '@@/js/collapsed.js';
import { defaultStore } from '@/store.js';
import { useRouter } from '@/router/supplier.js';
import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index c9c173aa35..56e8fcfa37 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -47,7 +47,7 @@ export type SuperMenuDef = {
active?: boolean;
action: (ev: MouseEvent) => void;
} | {
- type: 'link';
+ type?: 'link';
to: string;
icon?: string;
text: string;
diff --git a/packages/frontend/src/components/MkTagItem.stories.impl.ts b/packages/frontend/src/components/MkTagItem.stories.impl.ts
new file mode 100644
index 0000000000..3f243ff651
--- /dev/null
+++ b/packages/frontend/src/components/MkTagItem.stories.impl.ts
@@ -0,0 +1,70 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import MkTagItem from './MkTagItem.vue';
+
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkTagItem: MkTagItem,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ events() {
+ return {
+ click: action('click'),
+ exButtonClick: action('exButtonClick'),
+ };
+ },
+ },
+ template: '<MkTagItem v-bind="props" v-on="events"></MkTagItem>',
+ };
+ },
+ args: {
+ content: 'name',
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkTagItem>;
+
+export const Icon = {
+ ...Default,
+ args: {
+ ...Default.args,
+ iconClass: 'ti ti-arrow-up',
+ },
+} satisfies StoryObj<typeof MkTagItem>;
+
+export const ExButton = {
+ ...Default,
+ args: {
+ ...Default.args,
+ exButtonIconClass: 'ti ti-x',
+ },
+} satisfies StoryObj<typeof MkTagItem>;
+
+export const IconExButton = {
+ ...Default,
+ args: {
+ ...Default.args,
+ iconClass: 'ti ti-arrow-up',
+ exButtonIconClass: 'ti ti-x',
+ },
+} satisfies StoryObj<typeof MkTagItem>;
diff --git a/packages/frontend/src/components/MkTagItem.vue b/packages/frontend/src/components/MkTagItem.vue
new file mode 100644
index 0000000000..8b7460f3a3
--- /dev/null
+++ b/packages/frontend/src/components/MkTagItem.vue
@@ -0,0 +1,76 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root" @click="(ev) => emit('click', ev)">
+ <span v-if="iconClass" :class="[$style.icon, iconClass]"></span>
+ <span :class="$style.content">{{ content }}</span>
+ <MkButton v-if="exButtonIconClass" :class="$style.exButton" @click="(ev) => emit('exButtonClick', ev)">
+ <span :class="[$style.exButtonIcon, exButtonIconClass]"></span>
+ </MkButton>
+</div>
+</template>
+
+<script setup lang="ts">
+import MkButton from '@/components/MkButton.vue';
+
+const emit = defineEmits<{
+ (ev: 'click', payload: MouseEvent): void;
+ (ev: 'exButtonClick', payload: MouseEvent): void;
+}>();
+
+defineProps<{
+ iconClass?: string;
+ content: string;
+ exButtonIconClass?: string
+}>();
+</script>
+
+<style module lang="scss">
+$buttonSize : 1.8em;
+
+.root {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 9999px;
+ padding: 4px 6px;
+ gap: 3px;
+
+ background-color: var(--MI_THEME-buttonBg);
+
+ &:hover {
+ background-color: var(--MI_THEME-buttonHoverBg);
+ }
+}
+
+.icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.70em;
+}
+
+.exButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 9999px;
+ max-height: $buttonSize;
+ max-width: $buttonSize;
+ min-height: $buttonSize;
+ min-width: $buttonSize;
+ padding: 0;
+ box-sizing: border-box;
+ font-size: 0.65em;
+}
+
+.exButtonIcon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.80em;
+}
+</style>
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 260b83a743..3063c77c0e 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<div v-else-if="theNote" :class="[$style.link, { [$style.compact]: compact }]"><XNoteSimple :note="theNote" :class="$style.body"/></div>
-<div v-else>
+<div v-else-if="!hidePreview">
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
</div>
@@ -87,20 +87,22 @@ SPDX-License-Identifier: AGPL-3.0-only
import { defineAsyncComponent, onDeactivated, onUnmounted, ref, watch } from 'vue';
import { url as local } from '@@/js/config.js';
import { versatileLang } from '@@/js/intl-const.js';
+import * as Misskey from 'misskey-js';
import type { summaly } from '@misskey-dev/summaly';
+import type MkNoteSimple from '@/components/MkNoteSimple.vue';
+import type SkNoteSimple from '@/components/SkNoteSimple.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { deviceKind } from '@/scripts/device-kind.js';
import MkButton from '@/components/MkButton.vue';
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
import { defaultStore } from '@/store.js';
-import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js';
-const XNoteSimple = defineAsyncComponent(() =>
- (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteSimple.vue') :
- (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNoteSimple.vue') :
- null
+const XNoteSimple = defineAsyncComponent<typeof MkNoteSimple | typeof SkNoteSimple>(() =>
+ defaultStore.state.noteDesign === 'misskey'
+ ? import('@/components/MkNoteSimple.vue')
+ : import('@/components/SkNoteSimple.vue'),
);
type SummalyResult = Awaited<ReturnType<typeof summaly>>;
@@ -111,16 +113,19 @@ const props = withDefaults(defineProps<{
compact?: boolean;
showAsQuote?: boolean;
showActions?: boolean;
+ skipNoteIds?: (string | undefined)[];
}>(), {
detail: false,
compact: false,
showAsQuote: false,
showActions: true,
+ skipNoteIds: undefined,
});
const MOBILE_THRESHOLD = 500;
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
+const hidePreview = ref<boolean>(false);
const self = props.url.startsWith(local);
const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
@@ -155,6 +160,11 @@ watch(activityPub, async (uri) => {
try {
const response = await misskeyApi('ap/show', { uri });
if (response.type !== 'Note') return;
+ const theNoteId = response['object'].id;
+ if (theNoteId && props.skipNoteIds && props.skipNoteIds.includes(theNoteId)) {
+ hidePreview.value = true;
+ return;
+ }
theNote.value = response['object'];
} catch (err) {
if (_DEV_) {
@@ -382,11 +392,14 @@ onUnmounted(() => {
position: absolute;
width: 56px;
height: 100%;
+
+ & + .body {
+ left: 56px;
+ width: calc(100% - 56px);
+ }
}
> .body {
- left: 56px;
- width: calc(100% - 56px);
padding: 4px;
> .header {
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 85d4666172..120f19cb7f 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -16,16 +16,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>{{ i18n.ts.selectUser }}</template>
<div>
<div :class="$style.form">
- <MkInput v-if="localOnly" v-model="username" :autofocus="true" @update:modelValue="search">
+ <MkInput v-if="computedLocalOnly" v-model="username" :autofocus="true" debounce @update:modelValue="search">
<template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template>
</MkInput>
<FormSplit v-else :minWidth="170">
- <MkInput v-model="username" :autofocus="true" @update:modelValue="search">
+ <MkInput v-model="username" :autofocus="true" debounce @update:modelValue="search">
<template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template>
</MkInput>
- <MkInput v-model="host" :datalist="[hostname]" @update:modelValue="search">
+ <MkInput v-model="host" :datalist="[hostname]" debounce @update:modelValue="search">
<template #label>{{ i18n.ts.host }}</template>
<template #prefix>@</template>
</MkInput>
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, ref, shallowRef } from 'vue';
+import { onMounted, ref, computed, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import FormSplit from '@/components/form/split.vue';
@@ -70,6 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
import { host as currentHost, hostname } from '@@/js/config.js';
const emit = defineEmits<{
@@ -86,6 +87,8 @@ const props = withDefaults(defineProps<{
localOnly: false,
});
+const computedLocalOnly = computed(() => props.localOnly || instance.federation === 'none');
+
const username = ref('');
const host = ref('');
const users = ref<Misskey.entities.UserLite[]>([]);
@@ -98,10 +101,9 @@ function search() {
users.value = [];
return;
}
-
misskeyApi('users/search-by-username-and-host', {
username: username.value,
- host: props.localOnly ? '.' : host.value,
+ host: computedLocalOnly.value ? '.' : host.value,
limit: 10,
detail: false,
}).then(_users => {
@@ -143,7 +145,7 @@ onMounted(() => {
}).then(foundUsers => {
let _users = foundUsers;
_users = _users.filter((u) => {
- if (props.localOnly) {
+ if (computedLocalOnly.value) {
return u.host == null;
} else {
return true;
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 54f2ee655c..6d2a44e985 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -18,8 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="sanitizeHtml(instance.description) || i18n.ts.headlineMisskey"></div>
</div>
- <div v-if="instance.disableRegistration" :class="$style.mainWarn">
- <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
+ <div v-if="instance.disableRegistration || instance.federation !== 'all'" :class="$style.mainWarn" class="_gaps_s">
+ <MkInfo v-if="instance.disableRegistration" warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
+ <MkInfo v-if="instance.federation === 'specified'" warn>{{ i18n.ts.federationSpecified }}</MkInfo>
+ <MkInfo v-else-if="instance.federation === 'none'" warn>{{ i18n.ts.federationDisabled }}</MkInfo>
</div>
<div v-if="instance.approvalRequiredForSignup" :class="$style.mainWarn">
<MkInfo warn>{{ i18n.ts.approvalRequiredToRegister }}</MkInfo>
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index b987283a65..3446e3d6e2 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<header :class="$style.editHeader">
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
<template #label>{{ i18n.ts.selectWidget }}</template>
- <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
+ <option v-for="widget in _widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
</MkSelect>
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</Sortable>
</template>
- <component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
+ <component :is="`widget-${widget.name}`" v-for="widget in _widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
</div>
</template>
@@ -50,13 +50,14 @@ export type DefaultStoredWidget = {
</script>
<script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
-import { widgets as widgetDefs } from '@/widgets/index.js';
+import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
import { isLink } from '@@/js/is-link.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -66,6 +67,16 @@ const props = defineProps<{
edit: boolean;
}>();
+const _widgetDefs = computed(() => {
+ if (instance.federation === 'none') {
+ return widgetDefs.filter(x => !federationWidgets.includes(x));
+ } else {
+ return widgetDefs;
+ }
+});
+
+const _widgets = computed(() => props.widgets.filter(x => _widgetDefs.value.includes(x.name)));
+
const emit = defineEmits<{
(ev: 'updateWidgets', widgets: Widget[]): void;
(ev: 'addWidget', widget: Widget): void;
diff --git a/packages/frontend/src/components/SkErrorList.vue b/packages/frontend/src/components/SkErrorList.vue
new file mode 100644
index 0000000000..31d6596e3c
--- /dev/null
+++ b/packages/frontend/src/components/SkErrorList.vue
@@ -0,0 +1,43 @@
+<!--
+SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<!-- Match appearance of MkRemoteCaution.vue -->
+<div v-for="error of displayErrors" :key="error" :class="$style.root">
+ <i :class="$style.icon" class="ti ti-alert-triangle"></i>{{ i18n.ts._processErrors[error] ?? error }}
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { i18n } from '@/i18n.js';
+
+const props = defineProps<{
+ errors?: string[] | null;
+}>();
+
+const displayErrors = computed<Iterable<string>>(() => {
+ if (!props.errors?.length) return [];
+
+ // Set constructor preserve order, so we can sort first to avoid a copy operation.
+ return new Set(props.errors.toSorted());
+});
+</script>
+
+<style module lang="scss">
+.root {
+ font-size: 0.8em;
+ padding: 16px;
+ background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent);
+ color: var(--MI_THEME-infoWarnFg);
+ border-radius: var(--MI-radius);
+ overflow: clip;
+ z-index: 1;
+}
+
+.icon {
+ margin-right: 8px;
+}
+</style>
diff --git a/packages/frontend/src/components/SkFetchNote.vue b/packages/frontend/src/components/SkFetchNote.vue
new file mode 100644
index 0000000000..ab702c28f8
--- /dev/null
+++ b/packages/frontend/src/components/SkFetchNote.vue
@@ -0,0 +1,69 @@
+<!--
+SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkLazy @show="showing = true">
+ <MkLoading v-if="state === 'loading'"/>
+
+ <div v-if="state === 'error'">{{ i18n.ts.cannotLoadNote }}</div>
+
+ <DynamicNote v-if="state === 'done' && note" :note="note"/>
+</MkLazy>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
+import { i18n } from '@/i18n.js';
+import { misskeyApi } from '@/scripts/misskey-api';
+import DynamicNote from '@/components/DynamicNote.vue';
+
+const props = withDefaults(defineProps<{
+ noteId: string,
+ lazy?: boolean,
+}>(), {
+ lazy: true,
+});
+
+// Lazy-load, unless props.lazy is false.
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
+const showing = ref(!props.lazy);
+const state = ref<'loading' | 'error' | 'done'>('loading');
+const note = ref<Misskey.entities.Note | null>(null);
+
+watch(
+ [
+ () => props.noteId,
+ () => showing.value,
+ ],
+ async ([noteId, show]) => {
+ // Wait until the note is visible to avoid bombarding the API with requests.
+ if (!show) return;
+
+ // Unload the old note
+ note.value = null;
+ state.value = 'loading';
+
+ // Fetch the new note
+ const newNote = await misskeyApi('notes/show', { noteId }).catch(() => null);
+
+ // Check for race conditions (ex. the note changed again while the first request was still running)
+ if (noteId !== props.noteId) return;
+
+ // Check for errors
+ if (!newNote) {
+ state.value = 'error';
+ return;
+ }
+
+ // Display the new note
+ note.value = newNote;
+ state.value = 'done';
+ },
+ {
+ immediate: true,
+ },
+);
+</script>
diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue
index 2bfe5cc157..800b3afc65 100644
--- a/packages/frontend/src/components/SkInstanceTicker.vue
+++ b/packages/frontend/src/components/SkInstanceTicker.vue
@@ -4,40 +4,50 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="$style.root" :style="bg">
+<div :class="$style.root" :style="themeColorStyle">
<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/>
- <div :class="$style.name">{{ instance.name }}</div>
+ <div :class="$style.name">{{ instanceName }}</div>
</div>
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
-import { instanceName } from '@@/js/config.js';
-import { instance as Instance } from '@/instance.js';
+import { computed, type CSSProperties } from 'vue';
+import { instanceName as localInstanceName } from '@@/js/config.js';
+import { instance as localInstance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
+ host: string | null;
instance?: {
- faviconUrl?: string
- name: string
- themeColor?: string
+ faviconUrl?: string | null
+ name?: string | null
+ themeColor?: string | null
}
}>();
// if no instance data is given, this is for the local instance
-const instance = props.instance ?? {
- name: instanceName,
- themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
-};
+const instanceName = computed(() => props.host == null ? localInstanceName : props.instance?.name ?? props.host);
-const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
-
-const themeColor = instance.themeColor ?? '#777777';
+const faviconUrl = computed(() => {
+ let imageSrc: string | null = null;
+ if (props.host == null) {
+ if (localInstance.iconUrl == null) {
+ return '/favicon.ico';
+ } else {
+ imageSrc = localInstance.iconUrl;
+ }
+ } else {
+ imageSrc = props.instance?.faviconUrl ?? null;
+ }
+ return getProxiedImageUrlNullable(imageSrc);
+});
-const bg = {
- //background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
- background: `${themeColor}`,
-};
+const themeColorStyle = computed<CSSProperties>(() => {
+ const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777';
+ return {
+ background: `${themeColor}`,
+ };
+});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/SkMfmWindow.vue b/packages/frontend/src/components/SkMfmWindow.vue
index 58ca3d38ab..a628758a0f 100644
--- a/packages/frontend/src/components/SkMfmWindow.vue
+++ b/packages/frontend/src/components/SkMfmWindow.vue
@@ -382,6 +382,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div class="section _block">
+ <div class="title">{{ i18n.ts._mfm.border }}</div>
+ <div class="content">
+ <p>{{ i18n.ts._mfm.borderDescription }}</p>
+ <div class="preview">
+ <Mfm :text="preview_border"/>
+ <MkTextarea v-model="preview_border"><span>MFM</span></MkTextarea>
+ </div>
+ </div>
+ </div>
+ <div class="section _block">
<div class="title">{{ i18n.ts._mfm.plain }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.plainDescription }}</p>
@@ -421,7 +431,7 @@ const preview_center = ref(
);
const preview_inlineCode = ref('`<: "Hello, world!"`');
const preview_blockCode = ref(
- '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
+ '```ai\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
);
const preview_inlineMath = ref(
'\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)',
@@ -479,6 +489,7 @@ const preview_scale = ref(
);
const preview_fg = ref('$[fg.color=eb6f92 Text color]');
const preview_bg = ref('$[bg.color=31748f Background color]');
+const preview_border = ref('$[border.color=eb6f92,style=outset,width=10,radius=10 Border]');
const preview_plain = ref(
'<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>',
);
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index b4a9fa34cc..d4d4ca699d 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -62,10 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="[{ [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
<div style="container-type: inline-size;">
- <p v-if="appearNote.cw != null" :class="$style.cw">
+ <p v-if="mergedCW != null" :class="$style.cw">
<Mfm
- v-if="appearNote.cw != ''"
- :text="appearNote.cw"
+ v-if="mergedCW != ''"
+ :text="mergedCW"
:author="appearNote.user"
:nyaize="'respect'"
:enableEmojiMenu="true"
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
</p>
- <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
+ <div v-show="mergedCW == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<Mfm
@@ -102,9 +102,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/>
</div>
- <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" @click.stop/>
+ <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
<div v-if="isEnabledUrlPreview">
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :class="$style.urlPreview" @click.stop/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/>
</div>
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
@@ -129,11 +129,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
+ v-tooltip="renoteTooltip"
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop
- @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()"
+ @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
@@ -142,7 +143,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button
- v-if="canRenote && !props.mock"
+ v-if="canRenote && !props.mock && !$i?.rejectQuotes"
ref="quoteButton"
:class="$style.footerButton"
class="_button"
@@ -179,13 +180,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</template>
</I18n>
- <I18n v-else :src="i18n.ts.userSaysSomething" tag="small">
+ <I18n v-else-if="showSoftWordMutedWord !== true" :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
<MkUserName :user="appearNote.user"/>
</MkA>
</template>
</I18n>
+ <I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
+ <template #name>
+ <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
+ <MkUserName :user="appearNote.user"/>
+ </MkA>
+ </template>
+ <template #word>
+ {{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
+ </template>
+ </I18n>
</div>
<div v-else>
<!--
@@ -202,6 +213,7 @@ import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
import { shouldCollapsed } from '@@/js/collapsed.js';
import { host } from '@@/js/config.js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import type { MenuItem } from '@/types/menu.js';
import SkNoteSub from '@/components/SkNoteSub.vue';
import SkNoteHeader from '@/components/SkNoteHeader.vue';
@@ -238,7 +250,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { useRouter } from '@/router/supplier.js';
-import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@@ -318,6 +330,7 @@ const isDeleted = ref(false);
const renoted = ref(false);
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
+const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord);
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
@@ -333,6 +346,10 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
+const mergedCW = computed(() => computeMergedCw(appearNote.value));
+
+const renoteTooltip = computeRenoteTooltip(renoted);
+
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup',
url: `https://${host}/notes/${appearNote.value.id}`,
@@ -340,13 +357,18 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
/* Overload FunctionにLintが対応していないのでコメントアウト
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
-function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): Array<string | string[]> | false | 'sensitiveMute';
*/
-function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
if (mutedWords != null) {
- if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
- if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
- if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
+ const result = checkWordMute(noteToCheck, $i, mutedWords);
+ if (Array.isArray(result)) return result;
+
+ const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
+ if (Array.isArray(replyResult)) return replyResult;
+
+ const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
+ if (Array.isArray(renoteResult)) return renoteResult;
}
if (checkOnly) return false;
@@ -506,10 +528,10 @@ if (!props.mock) {
}
}
-function boostVisibility() {
+function boostVisibility(forceMenu: boolean = false) {
if (renoting) return;
- if (!defaultStore.state.showVisibilitySelectorOnBoost) {
+ if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 77a7e2afad..3787b54333 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -54,9 +54,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :nowrap="false" :user="appearNote.user"/>
</MkA>
<span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
- <span v-if="appearNote.user.badgeRoles" :class="$style.badgeRoles">
- <img v-for="role in appearNote.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
- </span>
</div>
<div :class="$style.noteHeaderUsernameAndBadgeRoles">
<div :class="$style.noteHeaderUsername">
@@ -79,15 +76,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="appearNote.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
</div>
- <SkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
+ <SkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
</div>
</div>
</header>
<div :class="$style.noteContent">
- <p v-if="appearNote.cw != null" :class="$style.cw">
+ <p v-if="mergedCW != null" :class="$style.cw">
<Mfm
- v-if="appearNote.cw != ''"
- :text="appearNote.cw"
+ v-if="mergedCW != ''"
+ :text="mergedCW"
:author="appearNote.user"
:nyaize="'respect'"
:enableEmojiMenu="true"
@@ -96,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
- <div v-show="appearNote.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<Mfm
v-if="appearNote.text"
@@ -123,9 +120,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
</div>
- <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll"/>
+ <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
<div v-if="isEnabledUrlPreview">
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" style="margin-top: 6px;"/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/>
</div>
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
</div>
@@ -148,10 +145,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
+ v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
- @mousedown.prevent="renoted ? undoRenote() : boostVisibility()"
+ @mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
@@ -160,7 +158,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button
- v-if="canRenote"
+ v-if="canRenote && !$i?.rejectQuotes"
ref="quoteButton"
class="_button"
:class="$style.noteFooterButton"
@@ -252,6 +250,7 @@ import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
import { host } from '@@/js/config.js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import SkNoteSub from '@/components/SkNoteSub.vue';
import SkNoteSimple from '@/components/SkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -288,7 +287,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
-import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
import { type Keymap } from '@/scripts/hotkey.js';
@@ -356,6 +355,10 @@ const quotes = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
+const mergedCW = computed(() => computeMergedCw(appearNote.value));
+
+const renoteTooltip = computeRenoteTooltip(renoted);
+
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
@@ -487,10 +490,10 @@ useTooltip(quoteButton, async (showing) => {
});
});
-function boostVisibility() {
+function boostVisibility(forceMenu: boolean = false) {
if (renoting) return;
- if (!defaultStore.state.showVisibilitySelectorOnBoost) {
+ if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
diff --git a/packages/frontend/src/components/SkNoteHeader.vue b/packages/frontend/src/components/SkNoteHeader.vue
index 6bcc30f6cb..cb50e57132 100644
--- a/packages/frontend/src/components/SkNoteHeader.vue
+++ b/packages/frontend/src/components/SkNoteHeader.vue
@@ -37,7 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></i></span>
</div>
- <div :class="$style.info"><SkInstanceTicker v-if="showTicker" style="cursor: pointer;" :instance="note.user.instance" @click.stop="showOnRemote()"/></div>
+ <div :class="$style.info">
+ <SkInstanceTicker v-if="showTicker" style="cursor: pointer;" :instance="note.user.instance" :host="note.user.host" @click.stop="showOnRemote()"/>
+ </div>
</div>
</header>
<header v-else :class="$style.classicRoot">
@@ -52,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
</div>
- <SkInstanceTicker v-if="showTicker && !isMobile && defaultStore.state.showTickerOnReplies" style="cursor: pointer; max-height: 5px; top: 3px; position: relative; margin-top: 0px !important;" :instance="note.user.instance" @click.stop="showOnRemote()"/>
+ <SkInstanceTicker v-if="showTicker && !isMobile && defaultStore.state.showTickerOnReplies" style="cursor: pointer; max-height: 5px; top: 3px; position: relative; margin-top: 0px !important;" :instance="note.user.instance" :host="note.user.host" @click.stop="showOnRemote()"/>
<div :class="$style.classicInfo">
<div v-if="mock">
<MkTime :time="note.createdAt" colored/>
@@ -84,6 +86,7 @@ import { popupMenu } from '@/os.js';
import { defaultStore } from '@/store.js';
import { useRouter } from '@/router/supplier.js';
import { deviceKind } from '@/scripts/device-kind.js';
+import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
const props = defineProps<{
note: Misskey.entities.Note;
diff --git a/packages/frontend/src/components/SkNoteSimple.vue b/packages/frontend/src/components/SkNoteSimple.vue
index b9895305f2..71a5bd4df8 100644
--- a/packages/frontend/src/components/SkNoteSimple.vue
+++ b/packages/frontend/src/components/SkNoteSimple.vue
@@ -9,11 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :classic="true" :note="note" :mini="true"/>
<div>
- <p v-if="note.cw != null" :class="$style.cw">
- <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :isBlock="true" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
+ <p v-if="mergedCW != null" :class="$style.cw">
+ <Mfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :isBlock="true" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
<MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/>
</p>
- <div v-show="note.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note" :expandAllCws="props.expandAllCws"/>
</div>
</div>
@@ -22,8 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch, ref } from 'vue';
+import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
@@ -37,6 +38,8 @@ const props = defineProps<{
let showContent = ref(defaultStore.state.uncollapseCW);
+const mergedCW = computed(() => computeMergedCw(props.note));
+
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index bd25e1e3ad..12703e1945 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -19,11 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.body">
<SkNoteHeader :class="$style.header" :note="note" :classic="true" :mini="true"/>
<div :class="$style.content">
- <p v-if="note.cw != null" :class="$style.cw">
- <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :isBlock="true" :author="note.user" :nyaize="'respect'"/>
+ <p v-if="mergedCW != null" :class="$style.cw">
+ <Mfm v-if="mergedCW != ''" style="margin-right: 8px;" :text="mergedCW" :isBlock="true" :author="note.user" :nyaize="'respect'"/>
<MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
</p>
- <div v-show="note.cw == null || showContent">
+ <div v-show="mergedCW == null || showContent">
<MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation" :expandAllCws="props.expandAllCws"/>
</div>
</div>
@@ -36,16 +36,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
+ v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
- @mousedown="renoted ? undoRenote() : boostVisibility()"
+ @mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
</button>
<button
- v-if="canRenote"
+ v-if="canRenote && !$i?.rejectQuotes"
ref="quoteButton"
class="_button"
:class="$style.noteFooterButton"
@@ -93,6 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
+import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import SkNoteHeader from '@/components/SkNoteHeader.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
@@ -114,7 +116,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
-import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
const hideLine = computed(() => { return props.detail ? true : false; });
@@ -149,10 +151,14 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
+const renoteTooltip = computeRenoteTooltip(renoted);
+
let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const replies = ref<Misskey.entities.Note[]>([]);
+const mergedCW = computed(() => computeMergedCw(appearNote.value));
+
const isRenote = (
props.note.renote != null &&
props.note.text == null &&
@@ -299,8 +305,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
-function boostVisibility() {
- if (!defaultStore.state.showVisibilitySelectorOnBoost) {
+function boostVisibility(forceMenu: boolean = false) {
+ if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index 9a1ac3aca2..2f4141b901 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { toUnicode } from 'punycode/';
+import { toUnicode } from 'punycode.js';
import { host as hostRaw } from '@@/js/config.js';
import { defaultStore } from '@/store.js';
diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue
index f35932ae77..29908f303d 100644
--- a/packages/frontend/src/components/global/MkLazy.vue
+++ b/packages/frontend/src/components/global/MkLazy.vue
@@ -16,10 +16,20 @@ import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } fr
const rootEl = shallowRef<HTMLDivElement>();
const showing = ref(false);
+const emit = defineEmits<{
+ (ev: 'show'): void,
+}>();
+
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
showing.value = true;
+
+ // Disconnect to avoid observer soft-leaks
+ observer.disconnect();
+
+ // Notify containing element to trigger edge logic
+ emit('show');
}
},
);
diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts
index 1039572a06..9785bc0f07 100644
--- a/packages/frontend/src/components/global/MkMfm.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { VNode, h, defineAsyncComponent, SetupContext, provide } from 'vue';
+import { VNode, h, defineAsyncComponent, SetupContext } from 'vue';
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
-import CkFollowMouse from '../CkFollowMouse.vue';
import { host } from '@@/js/config.js';
+import CkFollowMouse from '../CkFollowMouse.vue';
import MkUrl from '@/components/global/MkUrl.vue';
import MkTime from '@/components/global/MkTime.vue';
import MkLink from '@/components/MkLink.vue';
@@ -358,6 +358,10 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
}
}
+ case 'group': { // this is mostly a hack for the insides of `ruby`
+ style = '';
+ break;
+ }
case 'unixtime': {
const child = token.children[0];
const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 18c97b1bdb..1a424f349f 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -57,13 +57,16 @@ import { scrollToTop } from '@@/js/scroll.js';
import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
-import { PageHeaderItem } from '@/types/page-header.js';
+import type { PageHeaderItem } from '@/types/page-header.js';
+import type { PageMetadata } from '@/scripts/page-metadata.js';
const props = withDefaults(defineProps<{
+ overridePageMetadata?: PageMetadata;
tabs?: Tab[];
tab?: string;
actions?: PageHeaderItem[] | null;
thin?: boolean;
+ hideTitle?: boolean;
displayMyAvatar?: boolean;
displayBackButton?: boolean;
}>(), {
@@ -76,9 +79,10 @@ const emit = defineEmits<{
const displayBackButton = props.displayBackButton && history.state.key !== 'index' && history.length > 1 && inject('shouldBackButton', true);
-const pageMetadata = injectReactiveMetadata();
+const injectedPageMetadata = injectReactiveMetadata();
+const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
-const hideTitle = inject('shouldOmitHeaderTitle', false);
+const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props.hideTitle);
const thin_ = props.thin || inject('shouldHeaderThin', false);
const el = shallowRef<HTMLElement | undefined>(undefined);
@@ -87,7 +91,7 @@ const narrow = ref(false);
const hasTabs = computed(() => props.tabs.length > 0);
const hasActions = computed(() => props.actions && props.actions.length > 0);
const show = computed(() => {
- return !hideTitle || hasTabs.value || hasActions.value;
+ return !hideTitle.value || hasTabs.value || hasActions.value;
});
const preventDrag = (ev: TouchEvent) => {
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 8cca47c1db..5196a63635 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
-import { toUnicode as decodePunycode } from 'punycode/';
+import { toUnicode as decodePunycode } from 'punycode.js';
import { url as local } from '@@/js/config.js';
import * as os from '@/os.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
diff --git a/packages/frontend/src/components/grid/MkCellTooltip.vue b/packages/frontend/src/components/grid/MkCellTooltip.vue
new file mode 100644
index 0000000000..fd289c6cd9
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkCellTooltip.vue
@@ -0,0 +1,35 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="250" @closed="emit('closed')">
+ <div :class="$style.root">
+ {{ content }}
+ </div>
+</MkTooltip>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import MkTooltip from '@/components/MkTooltip.vue';
+
+defineProps<{
+ showing: boolean;
+ content: string;
+ targetElement: HTMLElement;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+ font-size: 0.9em;
+ text-align: left;
+ text-wrap: normal;
+}
+</style>
diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue
new file mode 100644
index 0000000000..e473b7c1af
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkDataCell.vue
@@ -0,0 +1,418 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ v-if="cell.row.using"
+ ref="rootEl"
+ class="mk_grid_td"
+ :class="$style.cell"
+ :style="{ maxWidth: cellWidth, minWidth: cellWidth }"
+ :tabindex="-1"
+ data-grid-cell
+ :data-grid-cell-row="cell.row.index"
+ :data-grid-cell-col="cell.column.index"
+ @keydown="onCellKeyDown"
+ @dblclick.prevent="onCellDoubleClick"
+>
+ <div
+ :class="[
+ $style.root,
+ [(cell.violation.valid || cell.selected) ? {} : $style.error],
+ [cell.selected ? $style.selected : {}],
+ // 行が選択されているときは範囲選択色の適用を行側に任せる
+ [(cell.ranged && !cell.row.ranged) ? $style.ranged : {}],
+ [needsContentCentering ? $style.center : {}],
+ ]"
+ >
+ <div v-if="!editing" :class="[$style.contentArea]" :style="cellType === 'boolean' ? 'justify-content: center' : ''">
+ <div ref="contentAreaEl" :class="$style.content">
+ <div v-if="cellType === 'text'">
+ {{ cell.value }}
+ </div>
+ <div v-if="cellType === 'number'">
+ {{ cell.value }}
+ </div>
+ <div v-if="cellType === 'date'">
+ {{ cell.value }}
+ </div>
+ <div v-else-if="cellType === 'boolean'">
+ <div :class="[$style.bool, {
+ [$style.boolTrue]: cell.value === true,
+ 'ti ti-check': cell.value === true,
+ }]"></div>
+ </div>
+ <div v-else-if="cellType === 'image'">
+ <img
+ :src="cell.value"
+ :alt="cell.value"
+ :class="$style.viewImage"
+ @load="emitContentSizeChanged"
+ />
+ </div>
+ </div>
+ </div>
+ <div v-else ref="inputAreaEl" :class="$style.inputArea">
+ <input
+ v-if="cellType === 'text'"
+ type="text"
+ :class="$style.editingInput"
+ :value="editingValue"
+ @input="onInputText"
+ @mousedown.stop
+ @contextmenu.stop
+ />
+ <input
+ v-if="cellType === 'number'"
+ type="number"
+ :class="$style.editingInput"
+ :value="editingValue"
+ @input="onInputText"
+ @mousedown.stop
+ @contextmenu.stop
+ />
+ <input
+ v-if="cellType === 'date'"
+ type="date"
+ :class="$style.editingInput"
+ :value="editingValue"
+ @input="onInputText"
+ @mousedown.stop
+ @contextmenu.stop
+ />
+ </div>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue';
+import { GridEventEmitter, Size } from '@/components/grid/grid.js';
+import { useTooltip } from '@/scripts/use-tooltip.js';
+import * as os from '@/os.js';
+import { CellValue, GridCell } from '@/components/grid/cell.js';
+import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
+import { GridRowSetting } from '@/components/grid/row.js';
+
+const emit = defineEmits<{
+ (ev: 'operation:beginEdit', sender: GridCell): void;
+ (ev: 'operation:endEdit', sender: GridCell): void;
+ (ev: 'change:value', sender: GridCell, newValue: CellValue): void;
+ (ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
+}>();
+const props = defineProps<{
+ cell: GridCell,
+ rowSetting: GridRowSetting,
+ bus: GridEventEmitter,
+}>();
+
+const { cell, bus } = toRefs(props);
+
+const rootEl = shallowRef<InstanceType<typeof HTMLTableCellElement>>();
+const contentAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>();
+const inputAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>();
+
+/** 値が編集中かどうか */
+const editing = ref<boolean>(false);
+/** 編集中の値. {@link beginEditing}と{@link endEditing}内、および各inputタグやそのコールバックからの操作のみを想定する */
+const editingValue = ref<CellValue>(undefined);
+
+const cellWidth = computed(() => cell.value.column.width);
+const cellType = computed(() => cell.value.column.setting.type);
+const needsContentCentering = computed(() => {
+ switch (cellType.value) {
+ case 'boolean':
+ return true;
+ default:
+ return false;
+ }
+});
+
+watch(() => [cell.value.value], () => {
+ // 中身がセットされた直後はサイズが分からないので、次のタイミングで更新する
+ nextTick(emitContentSizeChanged);
+}, { immediate: true });
+
+watch(() => cell.value.selected, () => {
+ if (cell.value.selected) {
+ requestFocus();
+ }
+});
+
+function onCellDoubleClick(ev: MouseEvent) {
+ switch (ev.type) {
+ case 'dblclick': {
+ beginEditing(ev.target as HTMLElement);
+ break;
+ }
+ }
+}
+
+function onOutsideMouseDown(ev: MouseEvent) {
+ const isOutside = ev.target instanceof Node && !rootEl.value?.contains(ev.target);
+ if (isOutside || !equalCellAddress(cell.value.address, getCellAddress(ev.target as HTMLElement))) {
+ endEditing(true, false);
+ }
+}
+
+function onCellKeyDown(ev: KeyboardEvent) {
+ if (!editing.value) {
+ ev.preventDefault();
+ switch (ev.code) {
+ case 'NumpadEnter':
+ case 'Enter':
+ case 'F2': {
+ beginEditing(ev.target as HTMLElement);
+ break;
+ }
+ }
+ } else {
+ switch (ev.code) {
+ case 'Escape': {
+ endEditing(false, true);
+ break;
+ }
+ case 'NumpadEnter':
+ case 'Enter': {
+ if (!ev.isComposing) {
+ endEditing(true, true);
+ }
+ }
+ }
+ }
+}
+
+function onInputText(ev: Event) {
+ editingValue.value = (ev.target as HTMLInputElement).value;
+}
+
+function onForceRefreshContentSize() {
+ emitContentSizeChanged();
+}
+
+function registerOutsideMouseDown() {
+ unregisterOutsideMouseDown();
+ addEventListener('mousedown', onOutsideMouseDown);
+}
+
+function unregisterOutsideMouseDown() {
+ removeEventListener('mousedown', onOutsideMouseDown);
+}
+
+async function beginEditing(target: HTMLElement) {
+ if (editing.value || !cell.value.selected || !cell.value.column.setting.editable) {
+ return;
+ }
+
+ if (cell.value.column.setting.customValueEditor) {
+ emit('operation:beginEdit', cell.value);
+ const newValue = await cell.value.column.setting.customValueEditor(
+ cell.value.row,
+ cell.value.column,
+ cell.value.value,
+ target,
+ );
+ emit('operation:endEdit', cell.value);
+
+ if (newValue !== cell.value.value) {
+ emitValueChange(newValue);
+ }
+
+ requestFocus();
+ } else {
+ switch (cellType.value) {
+ case 'number':
+ case 'date':
+ case 'text': {
+ editingValue.value = cell.value.value;
+ editing.value = true;
+ registerOutsideMouseDown();
+ emit('operation:beginEdit', cell.value);
+
+ await nextTick(() => {
+ // inputの展開後にフォーカスを当てたい
+ if (inputAreaEl.value) {
+ (inputAreaEl.value.querySelector('*') as HTMLElement).focus();
+ }
+ });
+ break;
+ }
+ case 'boolean': {
+ // とくに特殊なUIは設けず、トグルするだけ
+ emitValueChange(!cell.value.value);
+ break;
+ }
+ }
+ }
+}
+
+function endEditing(applyValue: boolean, requireFocus: boolean) {
+ if (!editing.value) {
+ return;
+ }
+
+ const newValue = editingValue.value;
+ editingValue.value = undefined;
+
+ emit('operation:endEdit', cell.value);
+ unregisterOutsideMouseDown();
+
+ if (applyValue && newValue !== cell.value.value) {
+ emitValueChange(newValue);
+ }
+
+ editing.value = false;
+
+ if (requireFocus) {
+ requestFocus();
+ }
+}
+
+function requestFocus() {
+ nextTick(() => {
+ rootEl.value?.focus();
+ });
+}
+
+function emitValueChange(newValue: CellValue) {
+ const _cell = cell.value;
+ emit('change:value', _cell, newValue);
+}
+
+function emitContentSizeChanged() {
+ emit('change:contentSize', cell.value, {
+ width: contentAreaEl.value?.clientWidth ?? 0,
+ height: contentAreaEl.value?.clientHeight ?? 0,
+ });
+}
+
+useTooltip(rootEl, (showing) => {
+ if (cell.value.violation.valid) {
+ return;
+ }
+
+ const content = cell.value.violation.violations.filter(it => !it.valid).map(it => it.result.message).join('\n');
+ const result = os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), {
+ showing,
+ content,
+ targetElement: rootEl.value!,
+ }, {
+ closed: () => {
+ result.dispose();
+ },
+ });
+});
+
+onMounted(() => {
+ bus.value.on('forceRefreshContentSize', onForceRefreshContentSize);
+});
+
+onUnmounted(() => {
+ bus.value.off('forceRefreshContentSize', onForceRefreshContentSize);
+});
+
+</script>
+
+<style module lang="scss">
+$cellHeight: 28px;
+
+.cell {
+ overflow: hidden;
+ white-space: nowrap;
+ height: $cellHeight;
+ max-height: $cellHeight;
+ min-height: $cellHeight;
+ cursor: cell;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.root {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ box-sizing: border-box;
+ height: 100%;
+
+ // selected適用時に中身がズレてしまうので、透明の線をあらかじめ引いておきたい
+ border: solid 0.5px transparent;
+
+ &.selected {
+ border: solid 0.5px var(--MI_THEME-accentLighten);
+ }
+
+ &.ranged {
+ background-color: var(--MI_THEME-accentedBg);
+ }
+
+ &.center {
+ justify-content: center;
+ }
+
+ &.error {
+ border: solid 0.5px var(--MI_THEME-error);
+ }
+}
+
+.contentArea, .inputArea {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ max-width: 100%;
+}
+
+.content {
+ display: inline-block;
+ padding: 0 8px;
+}
+
+.viewImage {
+ width: auto;
+ max-height: $cellHeight;
+ height: $cellHeight;
+ object-fit: cover;
+}
+
+.bool {
+ position: relative;
+ width: 18px;
+ height: 18px;
+ background: var(--MI_THEME-panel);
+ border: solid 2px var(--MI_THEME-divider);
+ border-radius: 4px;
+ box-sizing: border-box;
+
+ &.boolTrue {
+ border-color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-accent);
+
+ &::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: var(--MI_THEME-fgOnAccent);
+ font-size: 12px;
+ line-height: 18px;
+ }
+ }
+}
+
+.editingInput {
+ padding: 0 8px;
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+ min-height: $cellHeight - 2;
+ max-height: $cellHeight - 2;
+ height: $cellHeight - 2;
+ outline: none;
+ border: none;
+ font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
+}
+
+</style>
diff --git a/packages/frontend/src/components/grid/MkDataRow.vue b/packages/frontend/src/components/grid/MkDataRow.vue
new file mode 100644
index 0000000000..280a14bc4a
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkDataRow.vue
@@ -0,0 +1,72 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ class="mk_grid_tr"
+ :class="[
+ $style.row,
+ row.ranged ? $style.ranged : {},
+ ...(row.additionalStyles ?? []).map(it => it.className ?? {}),
+ ]"
+ :style="[
+ ...(row.additionalStyles ?? []).map(it => it.style ?? {}),
+ ]"
+ :data-grid-row="row.index"
+>
+ <MkNumberCell
+ v-if="setting.showNumber"
+ :content="(row.index + 1).toString()"
+ :row="row"
+ />
+ <MkDataCell
+ v-for="cell in cells"
+ :key="cell.address.col"
+ :vIf="cell.column.setting.type !== 'hidden'"
+ :cell="cell"
+ :rowSetting="setting"
+ :bus="bus"
+ @operation:beginEdit="(sender) => emit('operation:beginEdit', sender)"
+ @operation:endEdit="(sender) => emit('operation:endEdit', sender)"
+ @change:value="(sender, newValue) => emit('change:value', sender, newValue)"
+ @change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)"
+ />
+</div>
+</template>
+
+<script setup lang="ts">
+import { GridEventEmitter, Size } from '@/components/grid/grid.js';
+import MkDataCell from '@/components/grid/MkDataCell.vue';
+import MkNumberCell from '@/components/grid/MkNumberCell.vue';
+import { CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridRow, GridRowSetting } from '@/components/grid/row.js';
+
+const emit = defineEmits<{
+ (ev: 'operation:beginEdit', sender: GridCell): void;
+ (ev: 'operation:endEdit', sender: GridCell): void;
+ (ev: 'change:value', sender: GridCell, newValue: CellValue): void;
+ (ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
+}>();
+defineProps<{
+ row: GridRow,
+ cells: GridCell[],
+ setting: GridRowSetting,
+ bus: GridEventEmitter,
+}>();
+
+</script>
+
+<style module lang="scss">
+.row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: fit-content;
+
+ &.ranged {
+ background-color: var(--MI_THEME-accentedBg);
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/grid/MkGrid.stories.impl.ts b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts
new file mode 100644
index 0000000000..5801012f15
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts
@@ -0,0 +1,223 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import { ref } from 'vue';
+import { commonHandlers } from '../../../.storybook/mocks.js';
+import { boolean, choose, country, date, firstName, integer, lastName, text } from '../../../.storybook/fake-utils.js';
+import MkGrid from './MkGrid.vue';
+import { GridContext, GridEvent } from '@/components/grid/grid-event.js';
+import { DataSource, GridSetting } from '@/components/grid/grid.js';
+import { GridColumnSetting } from '@/components/grid/column.js';
+
+function d(p: {
+ check?: boolean,
+ name?: string,
+ email?: string,
+ age?: number,
+ birthday?: string,
+ gender?: string,
+ country?: string,
+ reportCount?: number,
+ createdAt?: string,
+}, seed: string) {
+ const prefix = text(10, seed);
+
+ return {
+ check: p.check ?? boolean(seed),
+ name: p.name ?? `${firstName(seed)} ${lastName(seed)}`,
+ email: p.email ?? `${prefix}@example.com`,
+ age: p.age ?? integer(20, 80, seed),
+ birthday: date({}, seed).toISOString(),
+ gender: p.gender ?? choose(['male', 'female', 'other', 'unknown'], seed),
+ country: p.country ?? country(seed),
+ reportCount: p.reportCount ?? integer(0, 9999, seed),
+ createdAt: p.createdAt ?? date({}, seed).toISOString(),
+ };
+}
+
+const defaultCols: GridColumnSetting[] = [
+ { bindTo: 'check', icon: 'ti-check', type: 'boolean', width: 50 },
+ { bindTo: 'name', title: 'Name', type: 'text', width: 'auto' },
+ { bindTo: 'email', title: 'Email', type: 'text', width: 'auto' },
+ { bindTo: 'age', title: 'Age', type: 'number', width: 50 },
+ { bindTo: 'birthday', title: 'Birthday', type: 'date', width: 'auto' },
+ { bindTo: 'gender', title: 'Gender', type: 'text', width: 80 },
+ { bindTo: 'country', title: 'Country', type: 'text', width: 120 },
+ { bindTo: 'reportCount', title: 'ReportCount', type: 'number', width: 'auto' },
+ { bindTo: 'createdAt', title: 'CreatedAt', type: 'date', width: 'auto' },
+];
+
+function createArgs(overrides?: { settings?: Partial<GridSetting>, data?: DataSource[] }) {
+ const refData = ref<ReturnType<typeof d>[]>([]);
+ for (let i = 0; i < 100; i++) {
+ refData.value.push(d({}, i.toString()));
+ }
+
+ return {
+ settings: {
+ row: overrides?.settings?.row,
+ cols: [
+ ...defaultCols.filter(col => overrides?.settings?.cols?.every(c => c.bindTo !== col.bindTo) ?? true),
+ ...overrides?.settings?.cols ?? [],
+ ],
+ cells: overrides?.settings?.cells,
+ },
+ data: refData.value,
+ };
+}
+
+function createRender(params: { settings: GridSetting, data: DataSource[] }) {
+ return {
+ render(args) {
+ return {
+ components: {
+ MkGrid,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ data() {
+ return {
+ data: args.data,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...args,
+ };
+ },
+ events() {
+ return {
+ event: (event: GridEvent, context: GridContext) => {
+ switch (event.type) {
+ case 'cell-value-change': {
+ args.data[event.row.index][event.column.setting.bindTo] = event.newValue;
+ }
+ }
+ },
+ };
+ },
+ },
+ template: '<div style="padding:20px"><MkGrid v-bind="props" v-on="events" /></div>',
+ };
+ },
+ args: {
+ ...params,
+ },
+ parameters: {
+ layout: 'fullscreen',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ ],
+ },
+ },
+ } satisfies StoryObj<typeof MkGrid>;
+}
+
+export const Default = createRender(createArgs());
+
+export const NoNumber = createRender(createArgs({
+ settings: {
+ row: {
+ showNumber: false,
+ },
+ },
+}));
+
+export const NoSelectable = createRender(createArgs({
+ settings: {
+ row: {
+ selectable: false,
+ },
+ },
+}));
+
+export const Editable = createRender(createArgs({
+ settings: {
+ cols: defaultCols.map(col => ({ ...col, editable: true })),
+ },
+}));
+
+export const AdditionalRowStyle = createRender(createArgs({
+ settings: {
+ cols: defaultCols.map(col => ({ ...col, editable: true })),
+ row: {
+ styleRules: [
+ {
+ condition: ({ row }) => AdditionalRowStyle.args.data[row.index].check as boolean,
+ applyStyle: {
+ style: {
+ backgroundColor: 'lightgray',
+ },
+ },
+ },
+ ],
+ },
+ },
+}));
+
+export const ContextMenu = createRender(createArgs({
+ settings: {
+ cols: [
+ {
+ bindTo: 'check', icon: 'ti-check', type: 'boolean', width: 50, contextMenuFactory: (col, context) => [
+ {
+ type: 'button',
+ text: 'Check All',
+ action: () => {
+ for (const d of ContextMenu.args.data) {
+ d.check = true;
+ }
+ },
+ },
+ {
+ type: 'button',
+ text: 'Uncheck All',
+ action: () => {
+ for (const d of ContextMenu.args.data) {
+ d.check = false;
+ }
+ },
+ },
+ ],
+ },
+ ],
+ row: {
+ contextMenuFactory: (row, context) => [
+ {
+ type: 'button',
+ text: 'Delete',
+ action: () => {
+ const idxes = context.rangedRows.map(r => r.index);
+ const newData = ContextMenu.args.data.filter((d, i) => !idxes.includes(i));
+
+ ContextMenu.args.data.splice(0);
+ ContextMenu.args.data.push(...newData);
+ },
+ },
+ ],
+ },
+ cells: {
+ contextMenuFactory: (col, row, value, context) => [
+ {
+ type: 'button',
+ text: 'Delete',
+ action: () => {
+ for (const cell of context.rangedCells) {
+ ContextMenu.args.data[cell.row.index][cell.column.setting.bindTo] = undefined;
+ }
+ },
+ },
+ ],
+ },
+ },
+}));
diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue
new file mode 100644
index 0000000000..4dbd4ebcae
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkGrid.vue
@@ -0,0 +1,1374 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ ref="rootEl"
+ class="mk_grid_border"
+ :class="[$style.grid, {
+ [$style.noOverflowHandling]: rootSetting.noOverflowStyle,
+ 'mk_grid_root_rounded': rootSetting.rounded,
+ 'mk_grid_root_border': rootSetting.outerBorder,
+ }]"
+ @mousedown.prevent="onMouseDown"
+ @keydown="onKeyDown"
+ @contextmenu.prevent.stop="onContextMenu"
+>
+ <div class="mk_grid_thead">
+ <MkHeaderRow
+ :columns="columns"
+ :gridSetting="rowSetting"
+ :bus="bus"
+ @operation:beginWidthChange="onHeaderCellWidthBeginChange"
+ @operation:endWidthChange="onHeaderCellWidthEndChange"
+ @operation:widthLargest="onHeaderCellWidthLargest"
+ @change:width="onHeaderCellChangeWidth"
+ @change:contentSize="onHeaderCellChangeContentSize"
+ />
+ </div>
+ <div class="mk_grid_tbody">
+ <MkDataRow
+ v-for="row in rows"
+ v-show="row.using"
+ :key="row.index"
+ :row="row"
+ :cells="cells[row.index].cells"
+ :setting="rowSetting"
+ :bus="bus"
+ :using="row.using"
+ :class="[lastLine === row.index ? 'last_row' : '']"
+ @operation:beginEdit="onCellEditBegin"
+ @operation:endEdit="onCellEditEnd"
+ @change:value="onChangeCellValue"
+ @change:contentSize="onChangeCellContentSize"
+ />
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, toRefs, watch } from 'vue';
+import { DataSource, GridEventEmitter, GridSetting, GridState, Size } from '@/components/grid/grid.js';
+import MkDataRow from '@/components/grid/MkDataRow.vue';
+import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
+import { cellValidation } from '@/components/grid/cell-validators.js';
+import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell, resetCell } from '@/components/grid/cell.js';
+import {
+ copyGridDataToClipboard,
+ equalCellAddress,
+ getCellAddress,
+ getCellElement,
+ pasteToGridFromClipboard,
+ removeDataFromGrid,
+} from '@/components/grid/grid-utils.js';
+import { MenuItem } from '@/types/menu.js';
+import * as os from '@/os.js';
+import { GridContext, GridEvent } from '@/components/grid/grid-event.js';
+import { createColumn, GridColumn } from '@/components/grid/column.js';
+import { createRow, defaultGridRowSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js';
+import { handleKeyEvent } from '@/scripts/key-event.js';
+
+type RowHolder = {
+ row: GridRow,
+ cells: GridCell[],
+ origin: DataSource,
+}
+
+const emit = defineEmits<{
+ (ev: 'event', event: GridEvent, context: GridContext): void;
+}>();
+
+const props = defineProps<{
+ settings: GridSetting;
+ data: DataSource[];
+}>();
+
+const rootSetting: Required<GridSetting['root']> = {
+ noOverflowStyle: false,
+ rounded: true,
+ outerBorder: true,
+ ...props.settings.root,
+};
+
+// non-reactive
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
+const rowSetting: Required<GridRowSetting> = {
+ ...defaultGridRowSetting,
+ ...props.settings.row,
+};
+
+// non-reactive
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
+const columnSettings = props.settings.cols;
+
+// non-reactive
+const cellSettings = props.settings.cells ?? {};
+
+const { data } = toRefs(props);
+
+// #region Event Definitions
+// region Event Definitions
+
+/**
+ * grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}。おもにpropsでの伝搬が難しいイベントを伝搬するために使用する。
+ * 子コンポーネント -> gridのイベントでは原則使用せず、{@link emit}を使用する。
+ */
+const bus = new GridEventEmitter();
+/**
+ * テーブルコンポーネントのリサイズイベントを監視するための{@link ResizeObserver}。
+ * 表示切替を検知し、サイズの再計算要求を発行するために使用する(マウント時にコンテンツが表示されていない場合、初手のサイズの自動計算が正常に働かないため)
+ *
+ * {@link setTimeout}を経由している理由は、{@link onResize}の中でサイズ再計算要求→サイズ変更が発生するとループとみなされ、
+ * 「ResizeObserver loop completed with undelivered notifications.」という警告が発生するため(再計算が完全に終われば通知は発生しなくなるので実際にはループしない)
+ *
+ * @see {@link onResize}
+ */
+const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
+
+const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
+/**
+ * グリッドの最も上位にある状態。
+ */
+const state = ref<GridState>('normal');
+/**
+ * グリッドの列定義。列定義の元の設定値は非リアクティブなので、初期値を生成して以降は変更しない。
+ */
+const columns = ref<GridColumn[]>(columnSettings.map(createColumn));
+/**
+ * グリッドの行定義。propsで受け取った{@link data}をもとに、{@link refreshData}で再計算される。
+ */
+const rows = ref<GridRow[]>([]);
+/**
+ * グリッドのセル定義。propsで受け取った{@link data}をもとに、{@link refreshData}で再計算される。
+ */
+const cells = ref<RowHolder[]>([]);
+
+/**
+ * mousemoveイベントが発生した際に、イベントから取得したセルアドレスを保持するための変数。
+ * セルアドレスが変わった瞬間にイベントを起こしたい時のために前回値として使用する。
+ */
+const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
+/**
+ * 編集中のセルのアドレスを保持するための変数。
+ */
+const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
+/**
+ * 列の範囲選択をする際の開始地点となるインデックスを保持するための変数。
+ * この開始地点からマウスが動いた地点までの範囲を選択する。
+ */
+const firstSelectionColumnIdx = ref<number>(CELL_ADDRESS_NONE.col);
+/**
+ * 行の範囲選択をする際の開始地点となるインデックスを保持するための変数。
+ * この開始地点からマウスが動いた地点までの範囲を選択する。
+ */
+const firstSelectionRowIdx = ref<number>(CELL_ADDRESS_NONE.row);
+
+/**
+ * 選択状態のセルを取得するための計算プロパティ。選択状態とは{@link GridCell.selected}がtrueのセルのこと。
+ */
+const selectedCell = computed(() => {
+ const selected = cells.value.flatMap(it => it.cells).filter(it => it.selected);
+ return selected.length > 0 ? selected[0] : undefined;
+});
+/**
+ * 範囲選択状態のセルを取得するための計算プロパティ。範囲選択状態とは{@link GridCell.ranged}がtrueのセルのこと。
+ */
+const rangedCells = computed(() => cells.value.flatMap(it => it.cells).filter(it => it.ranged));
+/**
+ * 範囲選択状態のセルの範囲を取得するための計算プロパティ。左上のセル番地と右下のセル番地を計算する。
+ */
+const rangedBounds = computed(() => {
+ const _cells = rangedCells.value;
+ const _cols = _cells.map(it => it.address.col);
+ const _rows = _cells.map(it => it.address.row);
+
+ const leftTop = {
+ col: Math.min(..._cols),
+ row: Math.min(..._rows),
+ };
+ const rightBottom = {
+ col: Math.max(..._cols),
+ row: Math.max(..._rows),
+ };
+
+ return {
+ leftTop,
+ rightBottom,
+ };
+});
+/**
+ * グリッドの中で使用可能なセルの範囲を取得するための計算プロパティ。左上のセル番地と右下のセル番地を計算する。
+ */
+const availableBounds = computed(() => {
+ const leftTop = {
+ col: 0,
+ row: 0,
+ };
+ const rightBottom = {
+ col: Math.max(...columns.value.map(it => it.index)),
+ row: Math.max(...rows.value.filter(it => it.using).map(it => it.index)),
+ };
+ return { leftTop, rightBottom };
+});
+/**
+ * 範囲選択状態の行を取得するための計算プロパティ。範囲選択状態とは{@link GridRow.ranged}がtrueの行のこと。
+ */
+const rangedRows = computed(() => rows.value.filter(it => it.ranged));
+
+const lastLine = computed(() => rows.value.filter(it => it.using).length - 1);
+
+// endregion
+// #endregion
+
+watch(data, patchData, { deep: true });
+
+if (_DEV_) {
+ watch(state, (value, oldValue) => {
+ console.log(`[grid][state] ${oldValue} -> ${value}`);
+ });
+}
+
+// #region Event Handlers
+// region Event Handlers
+
+function onResize(entries: ResizeObserverEntry[]) {
+ if (entries.length !== 1 || entries[0].target !== rootEl.value) {
+ return;
+ }
+
+ const contentRect = entries[0].contentRect;
+ if (_DEV_) {
+ console.log(`[grid][resize] contentRect: ${contentRect.width}x${contentRect.height}`);
+ }
+
+ switch (state.value) {
+ case 'hidden': {
+ if (contentRect.width > 0 && contentRect.height > 0) {
+ // 先に状態を変更しておき、再計算要求が複数回走らないようにする
+ state.value = 'normal';
+
+ // 選択状態が狂うかもしれないので解除しておく
+ unSelectionRangeAll();
+
+ // 再計算要求を発行。各セル側で最低限必要な横幅を算出し、emitで返してくるようになっている
+ bus.emit('forceRefreshContentSize');
+ }
+ break;
+ }
+ default: {
+ if (contentRect.width === 0 || contentRect.height === 0) {
+ state.value = 'hidden';
+ }
+ break;
+ }
+ }
+}
+
+function onKeyDown(ev: KeyboardEvent) {
+ const { ctrlKey, shiftKey, code } = ev;
+ if (_DEV_) {
+ console.log(`[grid][key] ctrl: ${ctrlKey}, shift: ${shiftKey}, code: ${code}`);
+ }
+
+ function updateSelectionRange(newBounds: { leftTop: CellAddress, rightBottom: CellAddress }) {
+ unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
+ expandCellRange(newBounds.leftTop, newBounds.rightBottom);
+ }
+
+ switch (state.value) {
+ case 'normal': {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ const selectedCellAddress = selectedCell.value?.address ?? CELL_ADDRESS_NONE;
+ const max = availableBounds.value;
+ const bounds = rangedBounds.value;
+
+ handleKeyEvent(ev, [
+ {
+ code: 'Delete', handler: () => {
+ if (rangedRows.value.length > 0) {
+ if (rowSetting.events.delete) {
+ rowSetting.events.delete(rangedRows.value);
+ }
+ } else {
+ const context = createContext();
+ removeDataFromGrid(context, (cell) => {
+ emitCellValue(cell, undefined);
+ });
+ }
+ },
+ },
+ {
+ code: 'KeyC', modifiers: ['Control'], handler: () => {
+ const context = createContext();
+ copyGridDataToClipboard(data.value, context);
+ },
+ },
+ {
+ code: 'KeyV', modifiers: ['Control'], handler: async () => {
+ const _cells = cells.value;
+ const context = createContext();
+ await pasteToGridFromClipboard(context, (row, col, parsedValue) => {
+ emitCellValue(_cells[row.index].cells[col.index], parsedValue);
+ });
+ },
+ },
+ {
+ code: 'ArrowRight', modifiers: ['Control', 'Shift'], handler: () => {
+ updateSelectionRange({
+ leftTop: { col: selectedCellAddress.col, row: bounds.leftTop.row },
+ rightBottom: { col: max.rightBottom.col, row: bounds.rightBottom.row },
+ });
+ },
+ },
+ {
+ code: 'ArrowLeft', modifiers: ['Control', 'Shift'], handler: () => {
+ updateSelectionRange({
+ leftTop: { col: max.leftTop.col, row: bounds.leftTop.row },
+ rightBottom: { col: selectedCellAddress.col, row: bounds.rightBottom.row },
+ });
+ },
+ },
+ {
+ code: 'ArrowUp', modifiers: ['Control', 'Shift'], handler: () => {
+ updateSelectionRange({
+ leftTop: { col: bounds.leftTop.col, row: max.leftTop.row },
+ rightBottom: { col: bounds.rightBottom.col, row: selectedCellAddress.row },
+ });
+ },
+ },
+ {
+ code: 'ArrowDown', modifiers: ['Control', 'Shift'], handler: () => {
+ updateSelectionRange({
+ leftTop: { col: bounds.leftTop.col, row: selectedCellAddress.row },
+ rightBottom: { col: bounds.rightBottom.col, row: max.rightBottom.row },
+ });
+ },
+ },
+ {
+ code: 'ArrowRight', modifiers: ['Shift'], handler: () => {
+ updateSelectionRange({
+ leftTop: {
+ col: bounds.leftTop.col < selectedCellAddress.col
+ ? bounds.leftTop.col + 1
+ : selectedCellAddress.col,
+ row: bounds.leftTop.row,
+ },
+ rightBottom: {
+ col: (bounds.rightBottom.col > selectedCellAddress.col || bounds.leftTop.col === selectedCellAddress.col)
+ ? bounds.rightBottom.col + 1
+ : selectedCellAddress.col,
+ row: bounds.rightBottom.row,
+ },
+ });
+ },
+ },
+ {
+ code: 'ArrowLeft', modifiers: ['Shift'], handler: () => {
+ updateSelectionRange({
+ leftTop: {
+ col: (bounds.leftTop.col < selectedCellAddress.col || bounds.rightBottom.col === selectedCellAddress.col)
+ ? bounds.leftTop.col - 1
+ : selectedCellAddress.col,
+ row: bounds.leftTop.row,
+ },
+ rightBottom: {
+ col: bounds.rightBottom.col > selectedCellAddress.col
+ ? bounds.rightBottom.col - 1
+ : selectedCellAddress.col,
+ row: bounds.rightBottom.row,
+ },
+ });
+ },
+ },
+ {
+ code: 'ArrowUp', modifiers: ['Shift'], handler: () => {
+ updateSelectionRange({
+ leftTop: {
+ col: bounds.leftTop.col,
+ row: (bounds.leftTop.row < selectedCellAddress.row || bounds.rightBottom.row === selectedCellAddress.row)
+ ? bounds.leftTop.row - 1
+ : selectedCellAddress.row,
+ },
+ rightBottom: {
+ col: bounds.rightBottom.col,
+ row: bounds.rightBottom.row > selectedCellAddress.row
+ ? bounds.rightBottom.row - 1
+ : selectedCellAddress.row,
+ },
+ });
+ },
+ },
+ {
+ code: 'ArrowDown', modifiers: ['Shift'], handler: () => {
+ updateSelectionRange({
+ leftTop: {
+ col: bounds.leftTop.col,
+ row: bounds.leftTop.row < selectedCellAddress.row
+ ? bounds.leftTop.row + 1
+ : selectedCellAddress.row,
+ },
+ rightBottom: {
+ col: bounds.rightBottom.col,
+ row: (bounds.rightBottom.row > selectedCellAddress.row || bounds.leftTop.row === selectedCellAddress.row)
+ ? bounds.rightBottom.row + 1
+ : selectedCellAddress.row,
+ },
+ });
+ },
+ },
+ {
+ code: 'ArrowDown', handler: () => {
+ selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 });
+ },
+ },
+ {
+ code: 'ArrowUp', handler: () => {
+ selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row - 1 });
+ },
+ },
+ {
+ code: 'ArrowRight', handler: () => {
+ selectionCell({ col: selectedCellAddress.col + 1, row: selectedCellAddress.row });
+ },
+ },
+ {
+ code: 'ArrowLeft', handler: () => {
+ selectionCell({ col: selectedCellAddress.col - 1, row: selectedCellAddress.row });
+ },
+ },
+ ]);
+
+ break;
+ }
+ }
+}
+
+function onMouseDown(ev: MouseEvent) {
+ switch (ev.button) {
+ case 0: {
+ onLeftMouseDown(ev);
+ break;
+ }
+ case 2: {
+ onRightMouseDown(ev);
+ break;
+ }
+ }
+}
+
+function onLeftMouseDown(ev: MouseEvent) {
+ const cellAddress = getCellAddress(ev.target as HTMLElement);
+ if (_DEV_) {
+ console.log(`[grid][mouse-left] state:${state.value}, button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
+ }
+
+ switch (state.value) {
+ case 'cellEditing': {
+ if (availableCellAddress(cellAddress) && !equalCellAddress(editingCellAddress.value, cellAddress)) {
+ selectionCell(cellAddress);
+ }
+ break;
+ }
+ case 'normal': {
+ if (availableCellAddress(cellAddress)) {
+ if (ev.shiftKey && selectedCell.value && !equalCellAddress(cellAddress, selectedCell.value.address)) {
+ const selectedCellAddress = selectedCell.value.address;
+
+ const leftTop = {
+ col: Math.min(selectedCellAddress.col, cellAddress.col),
+ row: Math.min(selectedCellAddress.row, cellAddress.row),
+ };
+
+ const rightBottom = {
+ col: Math.max(selectedCellAddress.col, cellAddress.col),
+ row: Math.max(selectedCellAddress.row, cellAddress.row),
+ };
+
+ unSelectionRangeAll();
+ expandCellRange(leftTop, rightBottom);
+
+ cells.value[selectedCellAddress.row].cells[selectedCellAddress.col].selected = true;
+ } else {
+ selectionCell(cellAddress);
+ }
+
+ previousCellAddress.value = cellAddress;
+
+ registerMouseUp();
+ registerMouseMove();
+ state.value = 'cellSelecting';
+ } else if (isColumnHeaderCellAddress(cellAddress)) {
+ if (ev.shiftKey) {
+ const rangedColumnIndexes = rangedCells.value.map(it => it.address.col);
+ const targetColumnIndexes = [cellAddress.col, ...rangedColumnIndexes];
+ unSelectionRangeAll();
+
+ const leftTop = {
+ col: Math.min(...targetColumnIndexes),
+ row: 0,
+ };
+
+ const rightBottom = {
+ col: Math.max(...targetColumnIndexes),
+ row: cells.value.length - 1,
+ };
+
+ expandCellRange(leftTop, rightBottom);
+
+ if (rangedColumnIndexes.length === 0) {
+ firstSelectionColumnIdx.value = cellAddress.col;
+ } else {
+ if (cellAddress.col > Math.min(...rangedColumnIndexes)) {
+ firstSelectionColumnIdx.value = Math.min(...rangedColumnIndexes);
+ } else {
+ firstSelectionColumnIdx.value = Math.max(...rangedColumnIndexes);
+ }
+ }
+ } else {
+ unSelectionRangeAll();
+
+ const colCells = cells.value.map(row => row.cells[cellAddress.col]);
+ selectionRange(...colCells.map(cell => cell.address));
+
+ firstSelectionColumnIdx.value = cellAddress.col;
+ }
+
+ registerMouseUp();
+ registerMouseMove();
+ previousCellAddress.value = cellAddress;
+ state.value = 'colSelecting';
+
+ // フォーカスを当てないとキーイベントが拾えないので
+ getCellElement(ev.target as HTMLElement)?.focus();
+ } else if (isRowNumberCellAddress(cellAddress)) {
+ if (ev.shiftKey) {
+ const rangedRowIndexes = rangedRows.value.map(it => it.index);
+ const targetRowIndexes = [cellAddress.row, ...rangedRowIndexes];
+ unSelectionRangeAll();
+
+ const leftTop = {
+ col: 0,
+ row: Math.min(...targetRowIndexes),
+ };
+
+ const rightBottom = {
+ col: Math.min(...cells.value.map(it => it.cells.length - 1)),
+ row: Math.max(...targetRowIndexes),
+ };
+
+ expandCellRange(leftTop, rightBottom);
+ expandRowRange(Math.min(...targetRowIndexes), Math.max(...targetRowIndexes));
+
+ if (rangedRowIndexes.length === 0) {
+ firstSelectionRowIdx.value = cellAddress.row;
+ } else {
+ if (cellAddress.col > Math.min(...rangedRowIndexes)) {
+ firstSelectionRowIdx.value = Math.min(...rangedRowIndexes);
+ } else {
+ firstSelectionRowIdx.value = Math.max(...rangedRowIndexes);
+ }
+ }
+ } else {
+ unSelectionRangeAll();
+ const rowCells = cells.value[cellAddress.row].cells;
+ selectionRange(...rowCells.map(cell => cell.address));
+ expandRowRange(cellAddress.row, cellAddress.row);
+
+ firstSelectionRowIdx.value = cellAddress.row;
+ }
+
+ registerMouseUp();
+ registerMouseMove();
+ previousCellAddress.value = cellAddress;
+ state.value = 'rowSelecting';
+
+ // フォーカスを当てないとキーイベントが拾えないので
+ getCellElement(ev.target as HTMLElement)?.focus();
+ }
+ break;
+ }
+ }
+}
+
+function onRightMouseDown(ev: MouseEvent) {
+ const cellAddress = getCellAddress(ev.target as HTMLElement);
+ if (_DEV_) {
+ console.log(`[grid][mouse-right] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
+ }
+
+ switch (state.value) {
+ case 'normal': {
+ if (!availableCellAddress(cellAddress)) {
+ return;
+ }
+
+ const _rangedCells = [...rangedCells.value];
+ if (!_rangedCells.some(it => equalCellAddress(it.address, cellAddress))) {
+ // 範囲選択外を右クリックした場合は、範囲選択を解除(範囲選択内であれば範囲選択を維持する)
+ selectionCell(cellAddress);
+ }
+
+ break;
+ }
+ }
+}
+
+function onMouseMove(ev: MouseEvent) {
+ ev.preventDefault();
+
+ const targetCellAddress = getCellAddress(ev.target as HTMLElement);
+ if (equalCellAddress(previousCellAddress.value, targetCellAddress)) {
+ // セルが変わるまでイベントを起こしたくない
+ return;
+ }
+
+ if (_DEV_) {
+ console.log(`[grid][mouse-move] button: ${ev.button}, cell: ${targetCellAddress.row}x${targetCellAddress.col}`);
+ }
+
+ switch (state.value) {
+ case 'cellSelecting': {
+ const selectedCellAddress = selectedCell.value?.address;
+ if (!availableCellAddress(targetCellAddress) || !selectedCellAddress) {
+ // 正しいセル範囲ではない
+ return;
+ }
+
+ const leftTop = {
+ col: Math.min(targetCellAddress.col, selectedCellAddress.col),
+ row: Math.min(targetCellAddress.row, selectedCellAddress.row),
+ };
+
+ const rightBottom = {
+ col: Math.max(targetCellAddress.col, selectedCellAddress.col),
+ row: Math.max(targetCellAddress.row, selectedCellAddress.row),
+ };
+
+ // 範囲外のセルは選択解除し、範囲内のセルは選択状態にする
+ unSelectionOutOfRange(leftTop, rightBottom);
+ expandCellRange(leftTop, rightBottom);
+ previousCellAddress.value = targetCellAddress;
+
+ break;
+ }
+ case 'colSelecting': {
+ if (!isColumnHeaderCellAddress(targetCellAddress) || previousCellAddress.value.col === targetCellAddress.col) {
+ // セルが変わるまでイベントを起こしたくない
+ return;
+ }
+
+ const leftTop = {
+ col: Math.min(targetCellAddress.col, firstSelectionColumnIdx.value),
+ row: 0,
+ };
+
+ const rightBottom = {
+ col: Math.max(targetCellAddress.col, firstSelectionColumnIdx.value),
+ row: cells.value.length - 1,
+ };
+
+ // 範囲外のセルは選択解除し、範囲内のセルは選択状態にする
+ unSelectionOutOfRange(leftTop, rightBottom);
+ expandCellRange(leftTop, rightBottom);
+ previousCellAddress.value = targetCellAddress;
+
+ // フォーカスを当てないとキーイベントが拾えないので
+ getCellElement(ev.target as HTMLElement)?.focus();
+
+ break;
+ }
+ case 'rowSelecting': {
+ if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) {
+ // セルが変わるまでイベントを起こしたくない
+ return;
+ }
+
+ const leftTop = {
+ col: 0,
+ row: Math.min(targetCellAddress.row, firstSelectionRowIdx.value),
+ };
+
+ const rightBottom = {
+ col: Math.min(...cells.value.map(it => it.cells.length - 1)),
+ row: Math.max(targetCellAddress.row, firstSelectionRowIdx.value),
+ };
+
+ // 範囲外のセルは選択解除し、範囲内のセルは選択状態にする
+ unSelectionOutOfRange(leftTop, rightBottom);
+ expandCellRange(leftTop, rightBottom);
+
+ // 行も同様に
+ const rangedRowIndexes = [rows.value[targetCellAddress.row].index, ...rangedRows.value.map(it => it.index)];
+ expandRowRange(Math.min(...rangedRowIndexes), Math.max(...rangedRowIndexes));
+
+ previousCellAddress.value = targetCellAddress;
+
+ // フォーカスを当てないとキーイベントが拾えないので
+ getCellElement(ev.target as HTMLElement)?.focus();
+
+ break;
+ }
+ }
+}
+
+function onMouseUp(ev: MouseEvent) {
+ ev.preventDefault();
+ switch (state.value) {
+ case 'rowSelecting':
+ case 'colSelecting':
+ case 'cellSelecting': {
+ unregisterMouseUp();
+ unregisterMouseMove();
+ state.value = 'normal';
+ previousCellAddress.value = CELL_ADDRESS_NONE;
+ break;
+ }
+ }
+}
+
+function onContextMenu(ev: MouseEvent) {
+ const cellAddress = getCellAddress(ev.target as HTMLElement);
+ if (_DEV_) {
+ console.log(`[grid][context-menu] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
+ }
+
+ const context = createContext();
+ const menuItems = Array.of<MenuItem>();
+ switch (true) {
+ // 通常セルのコンテキストメニュー作成
+ case availableCellAddress(cellAddress): {
+ const cell = cells.value[cellAddress.row].cells[cellAddress.col];
+ if (cell.setting.contextMenuFactory) {
+ menuItems.push(...cell.setting.contextMenuFactory(cell.column, cell.row, cell.value, context));
+ }
+ break;
+ }
+ // 列ヘッダセルのコンテキストメニュー作成
+ case isColumnHeaderCellAddress(cellAddress): {
+ const col = columns.value[cellAddress.col];
+ if (col.setting.contextMenuFactory) {
+ menuItems.push(...col.setting.contextMenuFactory(col, context));
+ }
+ break;
+ }
+ // 行ヘッダセルのコンテキストメニュー作成
+ case isRowNumberCellAddress(cellAddress): {
+ const row = rows.value[cellAddress.row];
+ if (row.setting.contextMenuFactory) {
+ menuItems.push(...row.setting.contextMenuFactory(row, context));
+ }
+ break;
+ }
+ }
+
+ if (menuItems.length > 0) {
+ os.contextMenu(menuItems, ev);
+ }
+}
+
+function onCellEditBegin(sender: GridCell) {
+ state.value = 'cellEditing';
+ editingCellAddress.value = sender.address;
+ for (const cell of cells.value.flatMap(it => it.cells)) {
+ if (cell.address.col !== sender.address.col || cell.address.row !== sender.address.row) {
+ // 編集状態となったセル以外は全部選択解除
+ cell.selected = false;
+ }
+ }
+}
+
+function onCellEditEnd() {
+ editingCellAddress.value = CELL_ADDRESS_NONE;
+ state.value = 'normal';
+}
+
+function onChangeCellValue(sender: GridCell, newValue: CellValue) {
+ applyRowRules([sender]);
+ emitCellValue(sender, newValue);
+}
+
+function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
+ const _cells = cells.value;
+ if (_cells.length > sender.address.row && _cells[sender.address.row].cells.length > sender.address.col) {
+ const currentSize = _cells[sender.address.row].cells[sender.address.col].contentSize;
+ if (currentSize.width !== contentSize.width || currentSize.height !== contentSize.height) {
+ // 通常セルのセル幅が確定したら、そのサイズを保持しておく(内容に引っ張られて想定よりも大きいセルサイズにならないようにするためのCSS作成に使用)
+ _cells[sender.address.row].cells[sender.address.col].contentSize = contentSize;
+
+ if (sender.column.setting.width === 'auto') {
+ calcLargestCellWidth(sender.column);
+ }
+ }
+ }
+}
+
+function onHeaderCellWidthBeginChange() {
+ switch (state.value) {
+ case 'normal': {
+ state.value = 'colResizing';
+ break;
+ }
+ }
+}
+
+function onHeaderCellWidthEndChange() {
+ switch (state.value) {
+ case 'colResizing': {
+ state.value = 'normal';
+ break;
+ }
+ }
+}
+
+function onHeaderCellChangeWidth(sender: GridColumn, width: string) {
+ switch (state.value) {
+ case 'colResizing': {
+ const column = columns.value[sender.index];
+ column.width = width;
+ break;
+ }
+ }
+}
+
+function onHeaderCellChangeContentSize(sender: GridColumn, newSize: Size) {
+ switch (state.value) {
+ case 'normal': {
+ const currentSize = columns.value[sender.index].contentSize;
+ if (currentSize.width !== newSize.width || currentSize.height !== newSize.height) {
+ // ヘッダセルのセル幅が確定したら、そのサイズを保持しておく(内容に引っ張られて想定よりも大きいセルサイズにならないようにするためのCSS作成に使用)
+ columns.value[sender.index].contentSize = newSize;
+
+ if (sender.setting.width === 'auto') {
+ calcLargestCellWidth(sender);
+ }
+ }
+ break;
+ }
+ }
+}
+
+function onHeaderCellWidthLargest(sender: GridColumn) {
+ switch (state.value) {
+ case 'normal': {
+ calcLargestCellWidth(sender);
+ break;
+ }
+ }
+}
+
+// endregion
+// #endregion
+
+// #region Methods
+// region Methods
+
+/**
+ * カラム内のコンテンツを表示しきるために必要な横幅と、各セルのコンテンツを表示しきるために必要な横幅を比較し、大きい方を列全体の横幅として採用する。
+ */
+function calcLargestCellWidth(column: GridColumn) {
+ const _cells = cells.value;
+ const largestColumnWidth = columns.value[column.index].contentSize.width;
+
+ const largestCellWidth = (_cells.length > 0)
+ ? _cells
+ .map(row => row.cells[column.index])
+ .reduce(
+ (acc, value) => Math.max(acc, value.contentSize.width),
+ 0,
+ )
+ : 0;
+
+ if (_DEV_) {
+ console.log(`[grid][calc-largest] idx:${column.setting.bindTo}, col:${largestColumnWidth}, cell:${largestCellWidth}`);
+ }
+
+ column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`;
+}
+
+/**
+ * {@link emit}を使用してイベントを発行する。
+ */
+function emitGridEvent(ev: GridEvent) {
+ const currentState: GridContext = {
+ selectedCell: selectedCell.value,
+ rangedCells: rangedCells.value,
+ rangedRows: rangedRows.value,
+ randedBounds: rangedBounds.value,
+ availableBounds: availableBounds.value,
+ state: state.value,
+ rows: rows.value,
+ columns: columns.value,
+ };
+
+ emit(
+ 'event',
+ ev,
+ currentState,
+ );
+}
+
+/**
+ * 親コンポーネントに新しい値を通知する。
+ * 新しい値は、イベント通知→元データへの反映→再計算(バリデーション含む)→再描画の流れで反映される。
+ */
+function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
+ const cellAddress = 'address' in sender ? sender.address : sender;
+ const cell = cells.value[cellAddress.row].cells[cellAddress.col];
+
+ emitGridEvent({
+ type: 'cell-value-change',
+ column: cell.column,
+ row: cell.row,
+ oldValue: cell.value,
+ newValue: newValue,
+ });
+
+ if (_DEV_) {
+ console.log(`[grid][cell-value] row:${cell.row}, col:${cell.column.index}, value:${newValue}`);
+ }
+}
+
+/**
+ * {@link target}のセルを選択状態にする。
+ * その際、{@link target}以外の行およびセルの範囲選択状態を解除する。
+ */
+function selectionCell(target: CellAddress) {
+ if (!availableCellAddress(target)) {
+ return;
+ }
+
+ unSelectionRangeAll();
+
+ const _cells = cells.value;
+ _cells[target.row].cells[target.col].selected = true;
+ _cells[target.row].cells[target.col].ranged = true;
+}
+
+/**
+ * {@link targets}のセルを範囲選択状態にする。
+ */
+function selectionRange(...targets: CellAddress[]) {
+ const _cells = cells.value;
+ for (const target of targets) {
+ const row = _cells[target.row];
+ if (row.row.using) {
+ row.cells[target.col].ranged = true;
+ }
+ }
+}
+
+/**
+ * 行およびセルの範囲選択状態をすべて解除する。
+ */
+function unSelectionRangeAll() {
+ const _cells = rangedCells.value;
+ for (const cell of _cells) {
+ cell.selected = false;
+ cell.ranged = false;
+ }
+
+ const _rows = rows.value.filter(it => it.using);
+ for (const row of _rows) {
+ row.ranged = false;
+ }
+}
+
+/**
+ * {@link leftTop}から{@link rightBottom}の範囲外にあるセルを範囲選択状態から外す。
+ */
+function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
+ const safeBounds = getSafeAddressBounds({ leftTop, rightBottom });
+
+ const _cells = rangedCells.value;
+ for (const cell of _cells) {
+ const outOfRangeCol = cell.address.col < safeBounds.leftTop.col || cell.address.col > safeBounds.rightBottom.col;
+ const outOfRangeRow = cell.address.row < safeBounds.leftTop.row || cell.address.row > safeBounds.rightBottom.row;
+ if (outOfRangeCol || outOfRangeRow) {
+ cell.ranged = false;
+ }
+ }
+
+ const outOfRangeRows = rows.value.filter((_, index) => index < safeBounds.leftTop.row || index > safeBounds.rightBottom.row);
+ for (const row of outOfRangeRows) {
+ row.ranged = false;
+ }
+}
+
+/**
+ * {@link leftTop}から{@link rightBottom}の範囲内にあるセルを範囲選択状態にする。
+ */
+function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
+ const safeBounds = getSafeAddressBounds({ leftTop, rightBottom });
+ const targetRows = cells.value.slice(safeBounds.leftTop.row, safeBounds.rightBottom.row + 1);
+ for (const row of targetRows) {
+ for (const cell of row.cells.slice(safeBounds.leftTop.col, safeBounds.rightBottom.col + 1)) {
+ cell.ranged = true;
+ }
+ }
+}
+
+/**
+ * {@link top}から{@link bottom}までの行を範囲選択状態にする。
+ */
+function expandRowRange(top: number, bottom: number) {
+ if (!rowSetting.selectable) {
+ return;
+ }
+
+ const targetRows = rows.value.slice(top, bottom + 1);
+ for (const row of targetRows) {
+ row.ranged = true;
+ }
+}
+
+/**
+ * 特定の条件下でのみ適用されるCSSを反映する。
+ */
+function applyRowRules(targetCells: GridCell[]) {
+ const _rows = rows.value;
+ const targetRowIdxes = [...new Set(targetCells.map(it => it.address.row))];
+ const rowGroups = Array.of<{ row: GridRow, cells: GridCell[] }>();
+ for (const rowIdx of targetRowIdxes) {
+ const rowGroup = targetCells.filter(it => it.address.row === rowIdx);
+ rowGroups.push({ row: _rows[rowIdx], cells: rowGroup });
+ }
+
+ const _cells = cells.value;
+ for (const group of rowGroups.filter(it => it.row.using)) {
+ const row = group.row;
+ const targetCols = group.cells.map(it => it.column);
+ const rowCells = _cells[group.row.index].cells;
+
+ const newStyles = rowSetting.styleRules
+ .filter(it => it.condition({ row, targetCols, cells: rowCells }))
+ .map(it => it.applyStyle);
+
+ if (JSON.stringify(newStyles) !== JSON.stringify(row.additionalStyles)) {
+ row.additionalStyles = newStyles;
+ }
+ }
+}
+
+function availableCellAddress(cellAddress: CellAddress): boolean {
+ const safeBounds = availableBounds.value;
+ return cellAddress.row >= safeBounds.leftTop.row &&
+ cellAddress.col >= safeBounds.leftTop.col &&
+ cellAddress.row <= safeBounds.rightBottom.row &&
+ cellAddress.col <= safeBounds.rightBottom.col;
+}
+
+function isColumnHeaderCellAddress(cellAddress: CellAddress): boolean {
+ return cellAddress.row === -1 && cellAddress.col >= 0;
+}
+
+function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
+ return cellAddress.row >= 0 && cellAddress.col === -1;
+}
+
+function getSafeAddressBounds(
+ bounds: { leftTop: CellAddress, rightBottom: CellAddress },
+): { leftTop: CellAddress, rightBottom: CellAddress } {
+ const available = availableBounds.value;
+
+ const safeLeftTop = {
+ col: Math.max(bounds.leftTop.col, available.leftTop.col),
+ row: Math.max(bounds.leftTop.row, available.leftTop.row),
+ };
+ const safeRightBottom = {
+ col: Math.min(bounds.rightBottom.col, available.rightBottom.col),
+ row: Math.min(bounds.rightBottom.row, available.rightBottom.row),
+ };
+
+ return { leftTop: safeLeftTop, rightBottom: safeRightBottom };
+}
+
+function registerMouseMove() {
+ unregisterMouseMove();
+ addEventListener('mousemove', onMouseMove);
+}
+
+function unregisterMouseMove() {
+ removeEventListener('mousemove', onMouseMove);
+}
+
+function registerMouseUp() {
+ unregisterMouseUp();
+ addEventListener('mouseup', onMouseUp);
+}
+
+function unregisterMouseUp() {
+ removeEventListener('mouseup', onMouseUp);
+}
+
+function createContext(): GridContext {
+ return {
+ selectedCell: selectedCell.value,
+ rangedCells: rangedCells.value,
+ rangedRows: rangedRows.value,
+ randedBounds: rangedBounds.value,
+ availableBounds: availableBounds.value,
+ state: state.value,
+ rows: rows.value,
+ columns: columns.value,
+ };
+}
+
+function refreshData() {
+ if (_DEV_) {
+ console.log('[grid][refresh-data][begin]');
+ }
+
+ // データを元に行・列・セルを作成する。
+ // 行は元データの配列の長さに応じて作成するが、最低限の行数は設定によって決まる。
+ // 行数が変わるたびに都度レンダリングするとパフォーマンスがイマイチなので、あらかじめ多めにセルを用意しておくための措置。
+ const _data: DataSource[] = data.value;
+ const _rows: GridRow[] = (_data.length > rowSetting.minimumDefinitionCount)
+ ? _data.map((_, index) => createRow(index, true, rowSetting))
+ : Array.from({ length: rowSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length, rowSetting));
+ const _cols: GridColumn[] = columns.value;
+
+ // 行・列の定義から、元データの配列より値を取得してセルを作成する。
+ // 行・列の定義はそれぞれインデックスを持っており、そのインデックスは元データの配列番地に対応している。
+ const _cells: RowHolder[] = _rows.map(row => {
+ const newCells = row.using
+ ? _cols.map(col => createCell(col, row, _data[row.index][col.setting.bindTo], cellSettings))
+ : _cols.map(col => createCell(col, row, undefined, cellSettings));
+
+ return { row, cells: newCells, origin: _data[row.index] };
+ });
+
+ rows.value = _rows;
+ cells.value = _cells;
+
+ const allCells = _cells.filter(it => it.row.using).flatMap(it => it.cells);
+ for (const cell of allCells) {
+ cell.violation = cellValidation(allCells, cell, cell.value);
+ }
+
+ applyRowRules(allCells);
+
+ if (_DEV_) {
+ console.log('[grid][refresh-data][end]');
+ }
+}
+
+/**
+ * セル値を部分更新する。この関数は、外部起因でデータが変更された場合に呼ばれる。
+ *
+ * 外部起因でデータが変更された場合は{@link data}の値が変更されるが、何処の番地がどのように変わったのかまでは検知できない。
+ * セルをすべて作り直せばいいが、その手法だと以下のデメリットがある。
+ * - 描画負荷がかかる
+ * - 各セルが持つ個別の状態(選択中状態やバリデーション結果など)が失われる
+ *
+ * そこで、新しい値とセルが持つ値を突き合わせ、変更があった場合のみ値を更新し、セルそのものは使いまわしつつ値を最新化する。
+ */
+function patchData(newItems: DataSource[]) {
+ if (_DEV_) {
+ console.log('[grid][patch-data][begin]');
+ }
+
+ const _cols = columns.value;
+
+ if (rows.value.length < newItems.length) {
+ const newRows = Array.of<GridRow>();
+ const newCells = Array.of<RowHolder>();
+
+ // 未使用の行を含めても足りないので新しい行を追加する
+ for (let rowIdx = rows.value.length; rowIdx < newItems.length; rowIdx++) {
+ const newRow = createRow(rowIdx, true, rowSetting);
+ newRows.push(newRow);
+ newCells.push({
+ row: newRow,
+ cells: _cols.map(col => createCell(col, newRow, newItems[rowIdx][col.setting.bindTo], cellSettings)),
+ origin: newItems[rowIdx],
+ });
+ }
+
+ rows.value.push(...newRows);
+ cells.value.push(...newCells);
+
+ applyRowRules(newCells.flatMap(it => it.cells));
+ }
+
+ // 行数の上限が欲しい場合はここに設けてもいいかもしれない
+
+ const usingRows = rows.value.filter(it => it.using);
+ if (usingRows.length > newItems.length) {
+ // 行数が減っているので古い行をクリアする(再マウント・再レンダリングが重いので要素そのものは消さない)
+ for (let rowIdx = newItems.length; rowIdx < usingRows.length; rowIdx++) {
+ resetRow(rows.value[rowIdx]);
+ for (let colIdx = 0; colIdx < _cols.length; colIdx++) {
+ const holder = cells.value[rowIdx];
+ holder.origin = {};
+ resetCell(holder.cells[colIdx]);
+ }
+ }
+ }
+
+ // 新しい値と既に設定されていた値を入れ替える
+ const changedCells = Array.of<GridCell>();
+ for (let rowIdx = 0; rowIdx < newItems.length; rowIdx++) {
+ const holder = cells.value[rowIdx];
+ holder.row.using = true;
+
+ const oldCells = holder.cells;
+ const newItem = newItems[rowIdx];
+ for (let colIdx = 0; colIdx < oldCells.length; colIdx++) {
+ const _col = columns.value[colIdx];
+
+ const oldCell = oldCells[colIdx];
+ const newValue = newItem[_col.setting.bindTo];
+ if (oldCell.value !== newValue) {
+ oldCell.value = _col.setting.valueTransformer
+ ? _col.setting.valueTransformer(holder.row, _col, newValue)
+ : newValue;
+ changedCells.push(oldCell);
+ }
+ }
+ }
+
+ if (changedCells.length > 0) {
+ const allCells = cells.value.slice(0, newItems.length).flatMap(it => it.cells);
+ for (const cell of allCells) {
+ cell.violation = cellValidation(allCells, cell, cell.value);
+ }
+
+ applyRowRules(changedCells);
+
+ // セル値が書き換わっており、バリデーションの結果も変わっているので外部に通知する必要がある
+ emitGridEvent({
+ type: 'cell-validation',
+ all: cells.value
+ .filter(it => it.row.using)
+ .flatMap(it => it.cells)
+ .map(it => it.violation)
+ .filter(it => !it.valid),
+ });
+ }
+
+ if (_DEV_) {
+ console.log('[grid][patch-data][end]');
+ }
+}
+
+// endregion
+// #endregion
+
+onMounted(() => {
+ state.value = 'normal';
+
+ const bindToList = columnSettings.map(it => it.bindTo);
+ if (new Set(bindToList).size !== columnSettings.length) {
+ // 取得元のプロパティ名重複は許容したくない
+ throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`);
+ }
+
+ if (rootEl.value) {
+ resizeObserver.observe(rootEl.value);
+
+ // 初期表示時にコンテンツが表示されていない場合はhidden状態にしておく。
+ // コンテンツ表示時にresizeイベントが発生するが、そのときにhidden状態にしておかないとサイズの再計算が走らないので
+ const bounds = rootEl.value.getBoundingClientRect();
+ if (bounds.width === 0 || bounds.height === 0) {
+ state.value = 'hidden';
+ }
+ }
+
+ refreshData();
+});
+</script>
+
+<style module lang="scss">
+.grid {
+ font-size: 90%;
+ overflow-x: scroll;
+ // firefoxだとスクロールバーがセルに重なって見づらくなってしまうのでスペースを空けておく
+ padding-bottom: 8px;
+
+ &.noOverflowHandling {
+ overflow-x: revert;
+ padding-bottom: 0;
+ }
+}
+</style>
+
+<style lang="scss">
+$borderSetting: solid 0.5px var(--MI_THEME-divider);
+
+// 配下コンポーネントを含めて一括してコントロールするため、scopedもmoduleも使用できない
+.mk_grid_border {
+ --rootBorderSetting: none;
+ --borderRadius: 0;
+
+ border-spacing: 0;
+
+ &.mk_grid_root_border {
+ --rootBorderSetting: #{$borderSetting};
+ }
+
+ &.mk_grid_root_rounded {
+ --borderRadius: var(--MI-radius);
+ }
+
+ .mk_grid_thead {
+ .mk_grid_tr {
+ .mk_grid_th {
+ border-left: $borderSetting;
+ border-top: var(--rootBorderSetting);
+
+ &:first-child {
+ // 左上セル
+ border-left: var(--rootBorderSetting);
+ border-top-left-radius: var(--borderRadius);
+ }
+
+ &:last-child {
+ // 右上セル
+ border-top-right-radius: var(--borderRadius);
+ border-right: var(--rootBorderSetting);
+ }
+ }
+ }
+ }
+
+ .mk_grid_tbody {
+ .mk_grid_tr {
+ .mk_grid_td, .mk_grid_th {
+ border-left: $borderSetting;
+ border-top: $borderSetting;
+
+ &:first-child {
+ // 左端の列
+ border-left: var(--rootBorderSetting);
+ }
+
+ &:last-child {
+ // 一番右端の列
+ border-right: var(--rootBorderSetting);
+ }
+ }
+ }
+
+ .last_row {
+ .mk_grid_td, .mk_grid_th {
+ // 一番下の行
+ border-bottom: var(--rootBorderSetting);
+
+ &:first-child {
+ // 左下セル
+ border-bottom-left-radius: var(--borderRadius);
+ }
+
+ &:last-child {
+ // 右下セル
+ border-bottom-right-radius: var(--borderRadius);
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/grid/MkHeaderCell.vue b/packages/frontend/src/components/grid/MkHeaderCell.vue
new file mode 100644
index 0000000000..aecfe7eaa3
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkHeaderCell.vue
@@ -0,0 +1,216 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ ref="rootEl"
+ class="mk_grid_th"
+ :class="$style.cell"
+ :style="[{ maxWidth: column.width, minWidth: column.width, width: column.width }]"
+ data-grid-cell
+ :data-grid-cell-row="-1"
+ :data-grid-cell-col="column.index"
+>
+ <div :class="$style.root">
+ <div :class="$style.left"></div>
+ <div :class="$style.wrapper">
+ <div ref="contentEl" :class="$style.contentArea">
+ <span v-if="column.setting.icon" class="ti" :class="column.setting.icon" style="line-height: normal"></span>
+ <span v-else>{{ text }}</span>
+ </div>
+ </div>
+ <div
+ :class="$style.right"
+ @mousedown="onHandleMouseDown"
+ @dblclick="onHandleDoubleClick"
+ ></div>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed, nextTick, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
+import { GridEventEmitter, Size } from '@/components/grid/grid.js';
+import { GridColumn } from '@/components/grid/column.js';
+
+const emit = defineEmits<{
+ (ev: 'operation:beginWidthChange', sender: GridColumn): void;
+ (ev: 'operation:endWidthChange', sender: GridColumn): void;
+ (ev: 'operation:widthLargest', sender: GridColumn): void;
+ (ev: 'change:width', sender: GridColumn, width: string): void;
+ (ev: 'change:contentSize', sender: GridColumn, newSize: Size): void;
+}>();
+const props = defineProps<{
+ column: GridColumn,
+ bus: GridEventEmitter,
+}>();
+
+const { column, bus } = toRefs(props);
+
+const rootEl = ref<InstanceType<typeof HTMLTableCellElement>>();
+const contentEl = ref<InstanceType<typeof HTMLDivElement>>();
+
+const resizing = ref<boolean>(false);
+
+const text = computed(() => {
+ const result = column.value.setting.title ?? column.value.setting.bindTo;
+ return result.length > 0 ? result : ' ';
+});
+
+watch(column, () => {
+ // 中身がセットされた直後はサイズが分からないので、次のタイミングで更新する
+ nextTick(emitContentSizeChanged);
+}, { immediate: true });
+
+function onHandleDoubleClick(ev: MouseEvent) {
+ switch (ev.type) {
+ case 'dblclick': {
+ emit('operation:widthLargest', column.value);
+ break;
+ }
+ }
+}
+
+function onHandleMouseDown(ev: MouseEvent) {
+ switch (ev.type) {
+ case 'mousedown': {
+ if (!resizing.value) {
+ registerHandleMouseUp();
+ registerHandleMouseMove();
+ resizing.value = true;
+ emit('operation:beginWidthChange', column.value);
+ }
+ break;
+ }
+ }
+}
+
+function onHandleMouseMove(ev: MouseEvent) {
+ if (!rootEl.value) {
+ // 型ガード
+ return;
+ }
+
+ switch (ev.type) {
+ case 'mousemove': {
+ if (resizing.value) {
+ const bounds = rootEl.value.getBoundingClientRect();
+ const clientWidth = rootEl.value.clientWidth;
+ const clientRight = bounds.left + clientWidth;
+ const nextWidth = clientWidth + (ev.clientX - clientRight);
+ emit('change:width', column.value, `${nextWidth}px`);
+ }
+ break;
+ }
+ }
+}
+
+function onHandleMouseUp(ev: MouseEvent) {
+ switch (ev.type) {
+ case 'mouseup': {
+ if (resizing.value) {
+ unregisterHandleMouseUp();
+ unregisterHandleMouseMove();
+ resizing.value = false;
+ emit('operation:endWidthChange', column.value);
+ }
+ break;
+ }
+ }
+}
+
+function onForceRefreshContentSize() {
+ emitContentSizeChanged();
+}
+
+function registerHandleMouseMove() {
+ unregisterHandleMouseMove();
+ addEventListener('mousemove', onHandleMouseMove);
+}
+
+function unregisterHandleMouseMove() {
+ removeEventListener('mousemove', onHandleMouseMove);
+}
+
+function registerHandleMouseUp() {
+ unregisterHandleMouseUp();
+ addEventListener('mouseup', onHandleMouseUp);
+}
+
+function unregisterHandleMouseUp() {
+ removeEventListener('mouseup', onHandleMouseUp);
+}
+
+function emitContentSizeChanged() {
+ const clientWidth = contentEl.value?.clientWidth ?? 0;
+ const clientHeight = contentEl.value?.clientHeight ?? 0;
+ emit('change:contentSize', column.value, {
+ // バーの横幅も考慮したいので、+3px
+ width: clientWidth + 3 + 3,
+ height: clientHeight,
+ });
+}
+
+onMounted(() => {
+ bus.value.on('forceRefreshContentSize', onForceRefreshContentSize);
+});
+
+onUnmounted(() => {
+ bus.value.off('forceRefreshContentSize', onForceRefreshContentSize);
+});
+
+</script>
+
+<style module lang="scss">
+$handleWidth: 5px;
+$cellHeight: 28px;
+
+.cell {
+ cursor: pointer;
+}
+
+.root {
+ display: flex;
+ flex-direction: row;
+ height: $cellHeight;
+ max-height: $cellHeight;
+ min-height: $cellHeight;
+
+ .wrapper {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ overflow: hidden;
+ justify-content: center;
+ }
+
+ .contentArea {
+ display: flex;
+ padding: 6px 4px;
+ box-sizing: border-box;
+ overflow: hidden;
+ white-space: nowrap;
+ text-align: center;
+ }
+
+ .left {
+ // rightのぶんだけズレるのでそれを相殺するためのネガティブマージン
+ margin-left: -$handleWidth;
+ margin-right: auto;
+ width: $handleWidth;
+ min-width: $handleWidth;
+ }
+
+ .right {
+ margin-left: auto;
+ // 判定を罫線の上に重ねたいのでネガティブマージンを使う
+ margin-right: -$handleWidth;
+ width: $handleWidth;
+ min-width: $handleWidth;
+ cursor: w-resize;
+ z-index: 1;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/grid/MkHeaderRow.vue b/packages/frontend/src/components/grid/MkHeaderRow.vue
new file mode 100644
index 0000000000..8affa08fd5
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkHeaderRow.vue
@@ -0,0 +1,60 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ class="mk_grid_tr"
+ :class="$style.root"
+ :data-grid-row="-1"
+>
+ <MkNumberCell
+ v-if="gridSetting.showNumber"
+ content="#"
+ :top="true"
+ />
+ <MkHeaderCell
+ v-for="column in columns"
+ :key="column.index"
+ :column="column"
+ :bus="bus"
+ @operation:beginWidthChange="(sender) => emit('operation:beginWidthChange', sender)"
+ @operation:endWidthChange="(sender) => emit('operation:endWidthChange', sender)"
+ @operation:widthLargest="(sender) => emit('operation:widthLargest', sender)"
+ @change:width="(sender, width) => emit('change:width', sender, width)"
+ @change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)"
+ />
+</div>
+</template>
+
+<script setup lang="ts">
+import { GridEventEmitter, Size } from '@/components/grid/grid.js';
+import MkHeaderCell from '@/components/grid/MkHeaderCell.vue';
+import MkNumberCell from '@/components/grid/MkNumberCell.vue';
+import { GridColumn } from '@/components/grid/column.js';
+import { GridRowSetting } from '@/components/grid/row.js';
+
+const emit = defineEmits<{
+ (ev: 'operation:beginWidthChange', sender: GridColumn): void;
+ (ev: 'operation:endWidthChange', sender: GridColumn): void;
+ (ev: 'operation:widthLargest', sender: GridColumn): void;
+ (ev: 'operation:selectionColumn', sender: GridColumn): void;
+ (ev: 'change:width', sender: GridColumn, width: string): void;
+ (ev: 'change:contentSize', sender: GridColumn, newSize: Size): void;
+}>();
+
+defineProps<{
+ columns: GridColumn[],
+ gridSetting: GridRowSetting,
+ bus: GridEventEmitter,
+}>();
+</script>
+
+<style module lang="scss">
+.root {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+</style>
diff --git a/packages/frontend/src/components/grid/MkNumberCell.vue b/packages/frontend/src/components/grid/MkNumberCell.vue
new file mode 100644
index 0000000000..674bba96bc
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkNumberCell.vue
@@ -0,0 +1,61 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ class="mk_grid_th"
+ :class="[$style.cell]"
+ :tabindex="-1"
+ data-grid-cell
+ :data-grid-cell-row="row?.index ?? -1"
+ :data-grid-cell-col="-1"
+>
+ <div :class="[$style.root]">
+ {{ content }}
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+
+import { GridRow } from '@/components/grid/row.js';
+
+defineProps<{
+ content: string,
+ row?: GridRow,
+}>();
+
+</script>
+
+<style module lang="scss">
+$cellHeight: 28px;
+$cellWidth: 34px;
+
+.cell {
+ overflow: hidden;
+ white-space: nowrap;
+ height: $cellHeight;
+ max-height: $cellHeight;
+ min-height: $cellHeight;
+ min-width: $cellWidth;
+ width: $cellWidth;
+ cursor: pointer;
+}
+
+.root {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ box-sizing: border-box;
+ padding: 0 8px;
+ height: 100%;
+ border: solid 0.5px transparent;
+
+ &.selected {
+ background-color: var(--MI_THEME-accentedBg);
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/grid/cell-validators.ts b/packages/frontend/src/components/grid/cell-validators.ts
new file mode 100644
index 0000000000..949cab2ec6
--- /dev/null
+++ b/packages/frontend/src/components/grid/cell-validators.ts
@@ -0,0 +1,110 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridColumn } from '@/components/grid/column.js';
+import { GridRow } from '@/components/grid/row.js';
+import { i18n } from '@/i18n.js';
+
+export type ValidatorParams = {
+ column: GridColumn;
+ row: GridRow;
+ value: CellValue;
+ allCells: GridCell[];
+};
+
+export type ValidatorResult = {
+ valid: boolean;
+ message?: string;
+}
+
+export type GridCellValidator = {
+ name?: string;
+ ignoreViolation?: boolean;
+ validate: (params: ValidatorParams) => ValidatorResult;
+}
+
+export type ValidateViolation = {
+ valid: boolean;
+ params: ValidatorParams;
+ violations: ValidateViolationItem[];
+}
+
+export type ValidateViolationItem = {
+ valid: boolean;
+ validator: GridCellValidator;
+ result: ValidatorResult;
+}
+
+export function cellValidation(allCells: GridCell[], cell: GridCell, newValue: CellValue): ValidateViolation {
+ const { column, row } = cell;
+ const validators = column.setting.validators ?? [];
+
+ const params: ValidatorParams = {
+ column,
+ row,
+ value: newValue,
+ allCells,
+ };
+
+ const violations: ValidateViolationItem[] = validators.map(validator => {
+ const result = validator.validate(params);
+ return {
+ valid: result.valid,
+ validator,
+ result,
+ };
+ });
+
+ return {
+ valid: violations.every(v => v.result.valid),
+ params,
+ violations,
+ };
+}
+
+class ValidatorPreset {
+ required(): GridCellValidator {
+ return {
+ name: 'required',
+ validate: ({ value }): ValidatorResult => {
+ return {
+ valid: value !== null && value !== undefined && value !== '',
+ message: i18n.ts._gridComponent._error.requiredValue,
+ };
+ },
+ };
+ }
+
+ regex(pattern: RegExp): GridCellValidator {
+ return {
+ name: 'regex',
+ validate: ({ value }): ValidatorResult => {
+ return {
+ valid: (typeof value !== 'string') || pattern.test(value.toString() ?? ''),
+ message: i18n.tsx._gridComponent._error.patternNotMatch({ pattern: pattern.source }),
+ };
+ },
+ };
+ }
+
+ unique(): GridCellValidator {
+ return {
+ name: 'unique',
+ validate: ({ column, row, value, allCells }): ValidatorResult => {
+ const bindTo = column.setting.bindTo;
+ const isUnique = allCells
+ .filter(it => it.column.setting.bindTo === bindTo && it.row.index !== row.index)
+ .every(cell => cell.value !== value);
+ return {
+ valid: isUnique,
+ message: i18n.ts._gridComponent._error.notUnique,
+ };
+ },
+ };
+ }
+}
+
+export const validators = new ValidatorPreset();
diff --git a/packages/frontend/src/components/grid/cell.ts b/packages/frontend/src/components/grid/cell.ts
new file mode 100644
index 0000000000..71b7a3e3f1
--- /dev/null
+++ b/packages/frontend/src/components/grid/cell.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ValidateViolation } from '@/components/grid/cell-validators.js';
+import { Size } from '@/components/grid/grid.js';
+import { GridColumn } from '@/components/grid/column.js';
+import { GridRow } from '@/components/grid/row.js';
+import { MenuItem } from '@/types/menu.js';
+import { GridContext } from '@/components/grid/grid-event.js';
+
+export type CellValue = string | boolean | number | undefined | null | Array<unknown> | NonNullable<unknown>;
+
+export type CellAddress = {
+ row: number;
+ col: number;
+}
+
+export const CELL_ADDRESS_NONE: CellAddress = {
+ row: -1,
+ col: -1,
+};
+
+export type GridCell = {
+ address: CellAddress;
+ value: CellValue;
+ column: GridColumn;
+ row: GridRow;
+ selected: boolean;
+ ranged: boolean;
+ contentSize: Size;
+ setting: GridCellSetting;
+ violation: ValidateViolation;
+}
+
+export type GridCellContextMenuFactory = (col: GridColumn, row: GridRow, value: CellValue, context: GridContext) => MenuItem[];
+
+export type GridCellSetting = {
+ contextMenuFactory?: GridCellContextMenuFactory;
+}
+
+export function createCell(
+ column: GridColumn,
+ row: GridRow,
+ value: CellValue,
+ setting: GridCellSetting,
+): GridCell {
+ const newValue = (row.using && column.setting.valueTransformer)
+ ? column.setting.valueTransformer(row, column, value)
+ : value;
+
+ return {
+ address: { row: row.index, col: column.index },
+ value: newValue,
+ column,
+ row,
+ selected: false,
+ ranged: false,
+ contentSize: { width: 0, height: 0 },
+ violation: {
+ valid: true,
+ params: {
+ column,
+ row,
+ value,
+ allCells: [],
+ },
+ violations: [],
+ },
+ setting,
+ };
+}
+
+export function resetCell(cell: GridCell): void {
+ cell.selected = false;
+ cell.ranged = false;
+ cell.violation = {
+ valid: true,
+ params: {
+ column: cell.column,
+ row: cell.row,
+ value: cell.value,
+ allCells: [],
+ },
+ violations: [],
+ };
+}
diff --git a/packages/frontend/src/components/grid/column.ts b/packages/frontend/src/components/grid/column.ts
new file mode 100644
index 0000000000..2f505756fe
--- /dev/null
+++ b/packages/frontend/src/components/grid/column.ts
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { GridCellValidator } from '@/components/grid/cell-validators.js';
+import { Size, SizeStyle } from '@/components/grid/grid.js';
+import { calcCellWidth } from '@/components/grid/grid-utils.js';
+import { CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridRow } from '@/components/grid/row.js';
+import { MenuItem } from '@/types/menu.js';
+import { GridContext } from '@/components/grid/grid-event.js';
+
+export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image' | 'hidden';
+
+export type CustomValueEditor = (row: GridRow, col: GridColumn, value: CellValue, cellElement: HTMLElement) => Promise<CellValue>;
+export type CellValueTransformer = (row: GridRow, col: GridColumn, value: CellValue) => CellValue;
+export type GridColumnContextMenuFactory = (col: GridColumn, context: GridContext) => MenuItem[];
+
+export type GridColumnSetting = {
+ bindTo: string;
+ title?: string;
+ icon?: string;
+ type: ColumnType;
+ width: SizeStyle;
+ editable?: boolean;
+ validators?: GridCellValidator[];
+ customValueEditor?: CustomValueEditor;
+ valueTransformer?: CellValueTransformer;
+ contextMenuFactory?: GridColumnContextMenuFactory;
+ events?: {
+ copy?: (value: CellValue) => string;
+ paste?: (text: string) => CellValue;
+ delete?: (cell: GridCell, context: GridContext) => void;
+ }
+};
+
+export type GridColumn = {
+ index: number;
+ setting: GridColumnSetting;
+ width: string;
+ contentSize: Size;
+}
+
+export function createColumn(setting: GridColumnSetting, index: number): GridColumn {
+ return {
+ index,
+ setting,
+ width: calcCellWidth(setting.width),
+ contentSize: { width: 0, height: 0 },
+ };
+}
+
diff --git a/packages/frontend/src/components/grid/grid-event.ts b/packages/frontend/src/components/grid/grid-event.ts
new file mode 100644
index 0000000000..074b72b956
--- /dev/null
+++ b/packages/frontend/src/components/grid/grid-event.ts
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridState } from '@/components/grid/grid.js';
+import { ValidateViolation } from '@/components/grid/cell-validators.js';
+import { GridColumn } from '@/components/grid/column.js';
+import { GridRow } from '@/components/grid/row.js';
+
+export type GridContext = {
+ selectedCell?: GridCell;
+ rangedCells: GridCell[];
+ rangedRows: GridRow[];
+ randedBounds: {
+ leftTop: CellAddress;
+ rightBottom: CellAddress;
+ };
+ availableBounds: {
+ leftTop: CellAddress;
+ rightBottom: CellAddress;
+ };
+ state: GridState;
+ rows: GridRow[];
+ columns: GridColumn[];
+};
+
+export type GridEvent =
+ GridCellValueChangeEvent |
+ GridCellValidationEvent
+ ;
+
+export type GridCellValueChangeEvent = {
+ type: 'cell-value-change';
+ column: GridColumn;
+ row: GridRow;
+ oldValue: CellValue;
+ newValue: CellValue;
+};
+
+export type GridCellValidationEvent = {
+ type: 'cell-validation';
+ violation?: ValidateViolation;
+ all: ValidateViolation[];
+};
diff --git a/packages/frontend/src/components/grid/grid-utils.ts b/packages/frontend/src/components/grid/grid-utils.ts
new file mode 100644
index 0000000000..a45bc88926
--- /dev/null
+++ b/packages/frontend/src/components/grid/grid-utils.ts
@@ -0,0 +1,215 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { isRef, Ref } from 'vue';
+import { DataSource, SizeStyle } from '@/components/grid/grid.js';
+import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridRow } from '@/components/grid/row.js';
+import { GridContext } from '@/components/grid/grid-event.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { GridColumn, GridColumnSetting } from '@/components/grid/column.js';
+
+export function isCellElement(elem: HTMLElement): boolean {
+ return elem.hasAttribute('data-grid-cell');
+}
+
+export function isRowElement(elem: HTMLElement): boolean {
+ return elem.hasAttribute('data-grid-row');
+}
+
+export function calcCellWidth(widthSetting: SizeStyle): string {
+ switch (widthSetting) {
+ case undefined:
+ case 'auto': {
+ return 'auto';
+ }
+ default: {
+ return `${widthSetting}px`;
+ }
+ }
+}
+
+function getCellRowByAttribute(elem: HTMLElement): number {
+ const row = elem.getAttribute('data-grid-cell-row');
+ if (row === null) {
+ throw new Error('data-grid-cell-row attribute not found');
+ }
+ return Number(row);
+}
+
+function getCellColByAttribute(elem: HTMLElement): number {
+ const col = elem.getAttribute('data-grid-cell-col');
+ if (col === null) {
+ throw new Error('data-grid-cell-col attribute not found');
+ }
+ return Number(col);
+}
+
+export function getCellAddress(elem: HTMLElement, parentNodeCount = 10): CellAddress {
+ let node = elem;
+ for (let i = 0; i < parentNodeCount; i++) {
+ if (!node.parentElement) {
+ break;
+ }
+
+ if (isCellElement(node) && isRowElement(node.parentElement)) {
+ const row = getCellRowByAttribute(node);
+ const col = getCellColByAttribute(node);
+
+ return { row, col };
+ }
+
+ node = node.parentElement;
+ }
+
+ return CELL_ADDRESS_NONE;
+}
+
+export function getCellElement(elem: HTMLElement, parentNodeCount = 10): HTMLElement | null {
+ let node = elem;
+ for (let i = 0; i < parentNodeCount; i++) {
+ if (isCellElement(node)) {
+ return node;
+ }
+
+ if (!node.parentElement) {
+ break;
+ }
+
+ node = node.parentElement;
+ }
+
+ return null;
+}
+
+export function equalCellAddress(a: CellAddress, b: CellAddress): boolean {
+ return a.row === b.row && a.col === b.col;
+}
+
+/**
+ * グリッドの選択範囲の内容をタブ区切り形式テキストに変換してクリップボードにコピーする。
+ */
+export function copyGridDataToClipboard(
+ gridItems: Ref<DataSource[]> | DataSource[],
+ context: GridContext,
+) {
+ const items = isRef(gridItems) ? gridItems.value : gridItems;
+ const lines = Array.of<string>();
+ const bounds = context.randedBounds;
+
+ for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
+ const rowItems = Array.of<string>();
+ for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
+ const { bindTo, events } = context.columns[col].setting;
+ const value = items[row][bindTo];
+ const transformValue = events?.copy
+ ? events.copy(value)
+ : typeof value === 'object' || Array.isArray(value)
+ ? JSON.stringify(value)
+ : value?.toString() ?? '';
+ rowItems.push(transformValue);
+ }
+ lines.push(rowItems.join('\t'));
+ }
+
+ const text = lines.join('\n');
+ copyToClipboard(text);
+
+ if (_DEV_) {
+ console.log(`Copied to clipboard: ${text}`);
+ }
+}
+
+/**
+ * クリップボードからタブ区切りテキストとして値を読み取り、グリッドの選択範囲に貼り付けるためのユーティリティ関数。
+ * …と言いつつも、使用箇所により反映方法に差があるため更新操作はコールバック関数に任せている。
+ */
+export async function pasteToGridFromClipboard(
+ context: GridContext,
+ callback: (row: GridRow, col: GridColumn, parsedValue: CellValue) => void,
+) {
+ function parseValue(value: string, setting: GridColumnSetting): CellValue {
+ if (setting.events?.paste) {
+ return setting.events.paste(value);
+ } else {
+ switch (setting.type) {
+ case 'number': {
+ return Number(value);
+ }
+ case 'boolean': {
+ return value === 'true';
+ }
+ default: {
+ return value;
+ }
+ }
+ }
+ }
+
+ const clipBoardText = await navigator.clipboard.readText();
+ if (_DEV_) {
+ console.log(`Paste from clipboard: ${clipBoardText}`);
+ }
+
+ const bounds = context.randedBounds;
+ const lines = clipBoardText.replace(/\r/g, '')
+ .split('\n')
+ .map(it => it.split('\t'));
+
+ if (lines.length === 1 && lines[0].length === 1) {
+ // 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
+ const ranges = context.rangedCells;
+ for (const cell of ranges) {
+ if (cell.column.setting.editable) {
+ callback(cell.row, cell.column, parseValue(lines[0][0], cell.column.setting));
+ }
+ }
+ } else {
+ // 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
+ const offsetRow = bounds.leftTop.row;
+ const offsetCol = bounds.leftTop.col;
+ const { columns, rows } = context;
+ for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
+ const rowIdx = row - offsetRow;
+ if (lines.length <= rowIdx) {
+ // クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
+ break;
+ }
+
+ const items = lines[rowIdx];
+ for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
+ const colIdx = col - offsetCol;
+ if (items.length <= colIdx) {
+ // クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
+ break;
+ }
+
+ if (columns[col].setting.editable) {
+ callback(rows[row], columns[col], parseValue(items[colIdx], columns[col].setting));
+ }
+ }
+ }
+ }
+}
+
+/**
+ * グリッドの選択範囲にあるデータを削除するためのユーティリティ関数。
+ * …と言いつつも、使用箇所により反映方法に差があるため更新操作はコールバック関数に任せている。
+ */
+export function removeDataFromGrid(
+ context: GridContext,
+ callback: (cell: GridCell) => void,
+) {
+ for (const cell of context.rangedCells) {
+ const { editable, events } = cell.column.setting;
+ if (editable) {
+ if (events?.delete) {
+ events.delete(cell, context);
+ } else {
+ callback(cell);
+ }
+ }
+ }
+}
diff --git a/packages/frontend/src/components/grid/grid.ts b/packages/frontend/src/components/grid/grid.ts
new file mode 100644
index 0000000000..b82e12b304
--- /dev/null
+++ b/packages/frontend/src/components/grid/grid.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { EventEmitter } from 'eventemitter3';
+import { CellValue, GridCellSetting } from '@/components/grid/cell.js';
+import { GridColumnSetting } from '@/components/grid/column.js';
+import { GridRowSetting } from '@/components/grid/row.js';
+
+export type GridSetting = {
+ root?: {
+ noOverflowStyle?: boolean;
+ rounded?: boolean;
+ outerBorder?: boolean;
+ };
+ row?: GridRowSetting;
+ cols: GridColumnSetting[];
+ cells?: GridCellSetting;
+};
+
+export type DataSource = Record<string, CellValue>;
+
+export type GridState =
+ 'normal' |
+ 'cellSelecting' |
+ 'cellEditing' |
+ 'colResizing' |
+ 'colSelecting' |
+ 'rowSelecting' |
+ 'hidden'
+ ;
+
+export type Size = {
+ width: number;
+ height: number;
+}
+
+export type SizeStyle = number | 'auto' | undefined;
+
+export type AdditionalStyle = {
+ className?: string;
+ style?: Record<string, string | number>;
+}
+
+export class GridEventEmitter extends EventEmitter<{
+ 'forceRefreshContentSize': void;
+}> {
+}
diff --git a/packages/frontend/src/components/grid/row.ts b/packages/frontend/src/components/grid/row.ts
new file mode 100644
index 0000000000..e0a317c9d3
--- /dev/null
+++ b/packages/frontend/src/components/grid/row.ts
@@ -0,0 +1,68 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { AdditionalStyle } from '@/components/grid/grid.js';
+import { GridCell } from '@/components/grid/cell.js';
+import { GridColumn } from '@/components/grid/column.js';
+import { MenuItem } from '@/types/menu.js';
+import { GridContext } from '@/components/grid/grid-event.js';
+
+export const defaultGridRowSetting: Required<GridRowSetting> = {
+ showNumber: true,
+ selectable: true,
+ minimumDefinitionCount: 100,
+ styleRules: [],
+ contextMenuFactory: () => [],
+ events: {},
+};
+
+export type GridRowStyleRuleConditionParams = {
+ row: GridRow,
+ targetCols: GridColumn[],
+ cells: GridCell[]
+};
+
+export type GridRowStyleRule = {
+ condition: (params: GridRowStyleRuleConditionParams) => boolean;
+ applyStyle: AdditionalStyle;
+}
+
+export type GridRowContextMenuFactory = (row: GridRow, context: GridContext) => MenuItem[];
+
+export type GridRowSetting = {
+ showNumber?: boolean;
+ selectable?: boolean;
+ minimumDefinitionCount?: number;
+ styleRules?: GridRowStyleRule[];
+ contextMenuFactory?: GridRowContextMenuFactory;
+ events?: {
+ delete?: (rows: GridRow[]) => void;
+ }
+}
+
+export type GridRow = {
+ index: number;
+ ranged: boolean;
+ using: boolean;
+ setting: GridRowSetting;
+ additionalStyles: AdditionalStyle[];
+}
+
+export function createRow(index: number, using: boolean, setting: GridRowSetting): GridRow {
+ return {
+ index,
+ ranged: false,
+ using: using,
+ setting,
+ additionalStyles: [],
+ };
+}
+
+export function resetRow(row: GridRow): void {
+ row.ranged = false;
+ row.using = false;
+ row.additionalStyles = [];
+}
+
diff --git a/packages/frontend/src/components/hook/useLoading.ts b/packages/frontend/src/components/hook/useLoading.ts
new file mode 100644
index 0000000000..6c6ff6ae0d
--- /dev/null
+++ b/packages/frontend/src/components/hook/useLoading.ts
@@ -0,0 +1,52 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed, h, ref } from 'vue';
+import MkLoading from '@/components/global/MkLoading.vue';
+
+export const useLoading = (props?: {
+ static?: boolean;
+ inline?: boolean;
+ colored?: boolean;
+ mini?: boolean;
+ em?: boolean;
+}) => {
+ const showingCnt = ref(0);
+
+ const show = () => {
+ showingCnt.value++;
+ };
+
+ const close = (force?: boolean) => {
+ if (force) {
+ showingCnt.value = 0;
+ } else {
+ showingCnt.value = Math.max(0, showingCnt.value - 1);
+ }
+ };
+
+ const scope = <T>(fn: () => T) => {
+ show();
+
+ const result = fn();
+ if (result instanceof Promise) {
+ return result.finally(() => close());
+ } else {
+ close();
+ return result;
+ }
+ };
+
+ const showing = computed(() => showingCnt.value > 0);
+ const component = computed(() => showing.value ? h(MkLoading, props) : null);
+
+ return {
+ show,
+ close,
+ scope,
+ component,
+ showing,
+ };
+};
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index 5f88acb11d..e5b1eff294 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps" :class="$style.textRoot">
<Mfm :text="block.text ?? ''" :isBlock="true" :isNote="false"/>
<div v-if="isEnabledUrlPreview" class="_gaps_s">
- <MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :showAsQuote="!page.user.rejectQuotes"/>
</div>
</div>
</template>
diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
deleted file mode 100644
index f9ce113687..0000000000
--- a/packages/frontend/src/index.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!--
- SPDX-FileCopyrightText: syuilo and misskey-project
- SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<!--
- 開発モードのviteはこのファイルを起点にサーバーを起動します。
- このファイルに書かれた [t]js のリンクと (s)cssのリンクと、その依存関係にあるファイルはビルドされます
--->
-
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="UTF-8" />
- <title>[DEV] Loading...</title>
- <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
- <meta
- http-equiv="Content-Security-Policy"
- content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
- worker-src 'self' blob:;
- script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh https://cdn.jsdelivr.net https://raw.esm.sh;
- style-src 'self' 'unsafe-inline';
- img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com;
- media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
- connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org https://api.friendlycaptcha.com https://raw.esm.sh;
- frame-src *;"
- />
- <meta property="og:site_name" content="[DEV BUILD] Sharkey" />
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta name="theme-color-orig" content="#86b300">
- <link rel='stylesheet' href='/assets/phosphor-icons/bold/style.css'>
- <link rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css'>
-</head>
-
-<body>
-<div id="sharkey_app"></div>
-<script type="module" src="./_dev_boot_.ts"></script>
-</body>
-</html>
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index a81f67aef3..59af5ad2b3 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -11,6 +11,7 @@ import * as Misskey from 'misskey-js';
import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/scripts/form.js';
import type { MenuItem } from '@/types/menu.js';
+import type { PostFormProps } from '@/types/post-form.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -28,15 +29,15 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
import { focusParent } from '@/scripts/focus.js';
-import type { PostFormProps } from '@/types/post-form.js';
export const openingWindowsCount = ref(0);
+export type ApiWithDialogCustomErrors = Record<string, { title?: string; text: string; }>;
export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
endpoint: E,
data: P,
token?: string | null | undefined,
- customErrors?: Record<string, { title?: string; text: string; }>,
+ customErrors?: ApiWithDialogCustomErrors,
) => {
const promise = misskeyApi(endpoint, data, token);
promiseDialog(promise, null, async (err) => {
@@ -610,6 +611,27 @@ export async function selectDriveFolder(multiple: boolean): Promise<Misskey.enti
});
}
+export async function selectRole(params: {
+ initialRoleIds?: string[],
+ title?: string,
+ infoMessage?: string,
+ publicOnly?: boolean,
+}): Promise<
+ { canceled: true; result: undefined; } |
+ { canceled: false; result: Misskey.entities.Role[] }
+> {
+ return new Promise((resolve) => {
+ popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, {
+ done: roles => {
+ resolve({ canceled: false, result: roles });
+ },
+ close: () => {
+ resolve({ canceled: true, result: undefined });
+ },
+ }, 'dispose');
+ });
+}
+
export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> {
return new Promise(resolve => {
const { dispose } = popup(MkEmojiPickerDialog, {
@@ -740,4 +762,3 @@ export function checkExistence(fileData: ArrayBuffer): Promise<any> {
});
});
}*/
-
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index f35cbe8d5a..1f36589a49 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
<XEmojis/>
</MkSpacer>
- <MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
+ <MkSpacer v-else-if="instance.federation !== 'none' && tab === 'federation'" :contentMax="1000" :marginMin="20">
<XFederation/>
</MkSpacer>
<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
@@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -51,22 +52,34 @@ watch(tab, () => {
const headerActions = computed(() => []);
-const headerTabs = computed(() => [{
- key: 'overview',
- title: i18n.ts.overview,
-}, {
- key: 'emojis',
- title: i18n.ts.customEmojis,
- icon: 'ph-smiley ph-bold ph-lg',
-}, {
- key: 'federation',
- title: i18n.ts.federation,
- icon: 'ti ti-whirl',
-}, {
- key: 'charts',
- title: i18n.ts.charts,
- icon: 'ti ti-chart-line',
-}]);
+const headerTabs = computed(() => {
+ const items = [];
+
+ items.push({
+ key: 'overview',
+ title: i18n.ts.overview,
+ }, {
+ key: 'emojis',
+ title: i18n.ts.customEmojis,
+ icon: 'ph-smiley ph-bold ph-lg',
+ });
+
+ if (instance.federation !== 'none') {
+ items.push({
+ key: 'federation',
+ title: i18n.ts.federation,
+ icon: 'ti ti-whirl',
+ });
+ }
+
+ items.push({
+ key: 'charts',
+ title: i18n.ts.charts,
+ icon: 'ti ti-chart-line',
+ });
+
+ return items;
+});
definePageMetadata(() => ({
title: i18n.ts.instanceInfo,
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 11a34d34ef..ce1fbc46a1 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue>
</div>
- <MkTextarea v-model="moderationNote" manualSave>
+ <MkTextarea v-model="moderationNote" manualSave @update:modelValue="onModerationNoteChanged">
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
@@ -81,8 +81,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
<MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
+ <MkSwitch v-model="rejectQuotes" @update:modelValue="toggleRejectQuotes">{{ user.host == null ? i18n.ts.rejectQuotesLocalUser : i18n.ts.rejectQuotesRemoteUser }}</MkSwitch>
<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
+ <MkInput v-model="mandatoryCW" type="text" manualSave @update:modelValue="onMandatoryCWChanged">
+ <template #label>{{ i18n.ts.mandatoryCW }}</template>
+ <template #caption>{{ i18n.ts.mandatoryCWDescription }}</template>
+ </MkInput>
+
<div>
<MkButton v-if="user.host == null && !isSystem" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
</div>
@@ -222,6 +228,7 @@ import { i18n } from '@/i18n.js';
import { iAmAdmin, $i, iAmModerator } from '@/account.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
+import MkInput from '@/components/MkInput.vue';
const props = withDefaults(defineProps<{
userId: string;
@@ -241,8 +248,10 @@ const moderator = ref(false);
const silenced = ref(false);
const approved = ref(false);
const suspended = ref(false);
+const rejectQuotes = ref(false);
const markedAsNSFW = ref(false);
const moderationNote = ref('');
+const mandatoryCW = ref<string | null>(null);
const isSystem = computed(() => info.value?.isSystem ?? false);
const filesPagination = {
endpoint: 'admin/drive/files' as const,
@@ -280,12 +289,9 @@ function createFetcher() {
approved.value = info.value.approved;
markedAsNSFW.value = info.value.alwaysMarkNsfw;
suspended.value = info.value.isSuspended;
+ rejectQuotes.value = user.value.rejectQuotes ?? false;
moderationNote.value = info.value.moderationNote;
-
- watch(moderationNote, async () => {
- await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
- await refreshUser();
- });
+ mandatoryCW.value = user.value.mandatoryCW;
});
}
@@ -293,6 +299,16 @@ function refreshUser() {
init.value = createFetcher();
}
+async function onMandatoryCWChanged(value: string) {
+ await os.apiWithDialog('admin/cw-user', { userId: props.userId, cw: value });
+ refreshUser();
+}
+
+async function onModerationNoteChanged(value: string) {
+ await misskeyApi('admin/update-user-note', { userId: props.userId, text: value });
+ refreshUser();
+}
+
async function updateRemoteUser() {
await os.apiWithDialog('federation/update-remote-user', { userId: user.value.id });
refreshUser();
@@ -355,6 +371,22 @@ async function toggleSuspend(v) {
}
}
+async function toggleRejectQuotes(v: boolean): Promise<void> {
+ const confirm = await os.confirm({
+ type: 'warning',
+ text: v ? i18n.ts.rejectQuotesConfirm : i18n.ts.allowQuotesConfirm,
+ });
+ if (confirm.canceled) {
+ rejectQuotes.value = !v;
+ } else {
+ await misskeyApi('admin/reject-quotes', {
+ userId: props.userId,
+ rejectQuotes: v,
+ });
+ await refreshUser();
+ }
+}
+
async function unsetUserAvatar() {
const confirm = await os.confirm({
type: 'warning',
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 2f6dac8097..e37df40f2f 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -14,13 +14,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else-if="botProtectionForm.savedState.provider === 'fc'" #suffix>FriendlyCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
- <template v-if="botProtectionForm.modified.value" #footer>
- <MkFormFooter :form="botProtectionForm"/>
+ <template #footer>
+ <MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/>
</template>
<div class="_gaps_m">
<MkRadios v-model="botProtectionForm.state.provider">
- <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
+ <option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
@@ -30,71 +30,126 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRadios>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
- <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey">
+ <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
- <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey">
+ <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
- <FormSlot>
- <template #label>{{ i18n.ts.preview }}</template>
- <MkCaptcha provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
+ <FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="hcaptcha"
+ :sitekey="botProtectionForm.state.hcaptchaSiteKey"
+ :secretKey="botProtectionForm.state.hcaptchaSecretKey"
+ />
</FormSlot>
+ <MkInfo>
+ <div :class="$style.captchaInfoMsg">
+ <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
+ <div>
+ <span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
+ </div>
+ </div>
+ </MkInfo>
</template>
+
<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
- <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey">
+ <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
</MkInput>
- <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey">
+ <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
</MkInput>
- <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl">
+ <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
- <template #label>{{ i18n.ts.preview }}</template>
- <MkCaptcha provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"/>
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="mcaptcha"
+ :sitekey="botProtectionForm.state.mcaptchaSiteKey"
+ :secretKey="botProtectionForm.state.mcaptchaSecretKey"
+ :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
+ />
</FormSlot>
</template>
+
<template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
- <MkInput v-model="botProtectionForm.state.recaptchaSiteKey">
+ <MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</MkInput>
- <MkInput v-model="botProtectionForm.state.recaptchaSecretKey">
+ <MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
- <template #label>{{ i18n.ts.preview }}</template>
- <MkCaptcha provider="recaptcha" :sitekey="botProtectionForm.state.recaptchaSiteKey"/>
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="recaptcha"
+ :sitekey="botProtectionForm.state.recaptchaSiteKey"
+ :secretKey="botProtectionForm.state.recaptchaSecretKey"
+ />
</FormSlot>
+ <MkInfo>
+ <div :class="$style.captchaInfoMsg">
+ <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
+ <div>
+ <span>ref: </span>
+ <a
+ href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
+ target="_blank"
+ >reCAPTCHA FAQ</a>
+ </div>
+ </div>
+ </MkInfo>
</template>
+
<template v-else-if="botProtectionForm.state.provider === 'turnstile'">
- <MkInput v-model="botProtectionForm.state.turnstileSiteKey">
+ <MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</MkInput>
- <MkInput v-model="botProtectionForm.state.turnstileSecretKey">
+ <MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</MkInput>
- <FormSlot>
- <template #label>{{ i18n.ts.preview }}</template>
- <MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/>
+ <FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="turnstile"
+ :sitekey="botProtectionForm.state.turnstileSiteKey"
+ :secretKey="botProtectionForm.state.turnstileSecretKey"
+ />
</FormSlot>
+ <MkInfo>
+ <div :class="$style.captchaInfoMsg">
+ <div>
+ {{ i18n.ts._captcha.testSiteKeyMessage }}
+ </div>
+ <div>
+ <span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
+ </div>
+ </div>
+ </MkInfo>
</template>
+
<template v-else-if="botProtectionForm.state.provider === 'fc'">
- <MkInput v-model="botProtectionForm.state.fcSiteKey">
+ <MkInput v-model="botProtectionForm.state.fcSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
- <MkInput v-model="botProtectionForm.state.fcSecretKey">
+ <MkInput v-model="botProtectionForm.state.fcSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
@@ -102,12 +157,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="fc" :sitekey="botProtectionForm.state.fcSiteKey"/>
</FormSlot>
+ <FormSlot v-if="botProtectionForm.state.fcSiteKey">
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="fc"
+ :sitekey="botProtectionForm.state.fcSiteKey"
+ :secretKey="botProtectionForm.state.fcSecretKey"
+ />
+ </FormSlot>
+ <MkInfo>
+ <div :class="$style.captchaInfoMsg">
+ <div>
+ {{ i18n.ts._captcha.testSiteKeyMessage }}
+ </div>
+ <div>
+ <span>ref: </span><a href="https://docs.friendlycaptcha.com/#/installation?id=_3-verifying-the-captcha-solution-on-the-server" target="_blank">FriendlyCaptcha Docs</a>
+ </div>
+ </div>
+ </MkInfo>
</template>
+
<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
<FormSlot>
- <template #label>{{ i18n.ts.preview }}</template>
- <MkCaptcha provider="testcaptcha"/>
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
</FormSlot>
</template>
</div>
@@ -115,7 +190,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
import MkRadios from '@/components/MkRadios.vue';
import MkInput from '@/components/MkInput.vue';
import FormSlot from '@/components/form/slot.vue';
@@ -127,56 +203,114 @@ import { useForm } from '@/scripts/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
+import { ApiWithDialogCustomErrors } from '@/os.js';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
-const meta = await misskeyApi('admin/meta');
+const errorHandler: ApiWithDialogCustomErrors = {
+ // 検証リクエストそのものに失敗
+ '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd': {
+ title: i18n.ts._captcha._error._requestFailed.title,
+ text: i18n.ts._captcha._error._requestFailed.text,
+ },
+ // 検証リクエストの結果が不正
+ 'c41c067f-24f3-4150-84b2-b5a3ae8c2214': {
+ title: i18n.ts._captcha._error._verificationFailed.title,
+ text: i18n.ts._captcha._error._verificationFailed.text,
+ },
+ // 不明なエラー
+ 'f868d509-e257-42a9-99c1-42614b031a97': {
+ title: i18n.ts._captcha._error._unknown.title,
+ text: i18n.ts._captcha._error._unknown.text,
+ },
+};
+
+const captchaResult = ref<string | null>(null);
+const meta = await misskeyApi('admin/captcha/current');
const botProtectionForm = useForm({
- provider: meta.enableHcaptcha
- ? 'hcaptcha'
- : meta.enableRecaptcha
- ? 'recaptcha'
- : meta.enableTurnstile
- ? 'turnstile'
- : meta.enableMcaptcha
- ? 'mcaptcha'
- : meta.enableFC
- ? 'fc'
- : meta.enableTestcaptcha
- ? 'testcaptcha'
- : null,
- hcaptchaSiteKey: meta.hcaptchaSiteKey,
- hcaptchaSecretKey: meta.hcaptchaSecretKey,
- mcaptchaSiteKey: meta.mcaptchaSiteKey,
- mcaptchaSecretKey: meta.mcaptchaSecretKey,
- mcaptchaInstanceUrl: meta.mcaptchaInstanceUrl,
- recaptchaSiteKey: meta.recaptchaSiteKey,
- recaptchaSecretKey: meta.recaptchaSecretKey,
- turnstileSiteKey: meta.turnstileSiteKey,
- turnstileSecretKey: meta.turnstileSecretKey,
- fcSiteKey: meta.fcSiteKey,
- fcSecretKey: meta.fcSecretKey,
+ provider: meta.provider,
+ hcaptchaSiteKey: meta.hcaptcha.siteKey,
+ hcaptchaSecretKey: meta.hcaptcha.secretKey,
+ mcaptchaSiteKey: meta.mcaptcha.siteKey,
+ mcaptchaSecretKey: meta.mcaptcha.secretKey,
+ mcaptchaInstanceUrl: meta.mcaptcha.instanceUrl,
+ recaptchaSiteKey: meta.recaptcha.siteKey,
+ recaptchaSecretKey: meta.recaptcha.secretKey,
+ turnstileSiteKey: meta.turnstile.siteKey,
+ turnstileSecretKey: meta.turnstile.secretKey,
+ fcSiteKey: meta.fc.siteKey,
+ fcSecretKey: meta.fc.secretKey,
}, async (state) => {
- await os.apiWithDialog('admin/update-meta', {
- enableHcaptcha: state.provider === 'hcaptcha',
- hcaptchaSiteKey: state.hcaptchaSiteKey,
- hcaptchaSecretKey: state.hcaptchaSecretKey,
- enableMcaptcha: state.provider === 'mcaptcha',
- mcaptchaSiteKey: state.mcaptchaSiteKey,
- mcaptchaSecretKey: state.mcaptchaSecretKey,
- mcaptchaInstanceUrl: state.mcaptchaInstanceUrl,
- enableRecaptcha: state.provider === 'recaptcha',
- recaptchaSiteKey: state.recaptchaSiteKey,
- recaptchaSecretKey: state.recaptchaSecretKey,
- enableTurnstile: state.provider === 'turnstile',
- turnstileSiteKey: state.turnstileSiteKey,
- turnstileSecretKey: state.turnstileSecretKey,
- enableFC: state.provider === 'fc',
- fcSiteKey: state.fcSiteKey,
- fcSecretKey: state.fcSecretKey,
- enableTestcaptcha: state.provider === 'testcaptcha',
- });
- fetchInstance(true);
+ const provider = state.provider;
+ if (provider === 'none') {
+ await os.apiWithDialog(
+ 'admin/captcha/save',
+ { provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'] },
+ undefined,
+ errorHandler,
+ );
+ } else {
+ const sitekey = provider === 'hcaptcha'
+ ? state.hcaptchaSiteKey
+ : provider === 'mcaptcha'
+ ? state.mcaptchaSiteKey
+ : provider === 'recaptcha'
+ ? state.recaptchaSiteKey
+ : provider === 'turnstile'
+ ? state.turnstileSiteKey
+ : provider === 'fc'
+ ? state.fcSiteKey
+ : null;
+ const secret = provider === 'hcaptcha'
+ ? state.hcaptchaSecretKey
+ : provider === 'mcaptcha'
+ ? state.mcaptchaSecretKey
+ : provider === 'recaptcha'
+ ? state.recaptchaSecretKey
+ : provider === 'turnstile'
+ ? state.turnstileSecretKey
+ : provider === 'fc'
+ ? state.fcSecretKey
+ : null;
+
+ await os.apiWithDialog(
+ 'admin/captcha/save',
+ {
+ provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'],
+ sitekey: sitekey,
+ secret: secret,
+ instanceUrl: state.mcaptchaInstanceUrl,
+ captchaResult: captchaResult.value,
+ },
+ undefined,
+ errorHandler,
+ );
+ }
+
+ await fetchInstance(true);
});
+
+watch(botProtectionForm.state, () => {
+ captchaResult.value = null;
+});
+
+const canSaving = computed((): boolean => {
+ return (botProtectionForm.state.provider === 'none') ||
+ (botProtectionForm.state.provider === 'hcaptcha' && !!captchaResult.value) ||
+ (botProtectionForm.state.provider === 'mcaptcha' && !!captchaResult.value) ||
+ (botProtectionForm.state.provider === 'recaptcha' && !!captchaResult.value) ||
+ (botProtectionForm.state.provider === 'turnstile' && !!captchaResult.value) ||
+ (botProtectionForm.state.provider === 'fc' && !!captchaResult.value) ||
+ (botProtectionForm.state.provider === 'testcaptcha' && !!captchaResult.value);
+});
+
</script>
+
+<style lang="scss" module>
+.captchaInfoMsg {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts
new file mode 100644
index 0000000000..141ab858d3
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts
@@ -0,0 +1,57 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type RequestLogItem = {
+ failed: boolean;
+ url: string;
+ name: string;
+ error?: string;
+};
+
+export const gridSortOrderKeys = [
+ 'name',
+ 'category',
+ 'aliases',
+ 'type',
+ 'license',
+ 'host',
+ 'uri',
+ 'publicUrl',
+ 'isSensitive',
+ 'localOnly',
+ 'updatedAt',
+] as const satisfies string[];
+
+export type GridSortOrderKey = typeof gridSortOrderKeys[number];
+
+export function emptyStrToUndefined(value: string | null) {
+ return value ? value : undefined;
+}
+
+export function emptyStrToNull(value: string) {
+ return value === '' ? null : value;
+}
+
+export function emptyStrToEmptyArray(value: string) {
+ return value === '' ? [] : value.split(' ').map(it => it.trim());
+}
+
+export function roleIdsParser(text: string): { id: string, name: string }[] {
+ // idとnameのペア配列をJSONで受け取る。それ以外の形式は許容しない
+ try {
+ const obj = JSON.parse(text);
+ if (!Array.isArray(obj)) {
+ return [];
+ }
+ if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) {
+ return [];
+ }
+
+ return obj.map(it => ({ id: it.id, name: it.name }));
+ } catch (ex) {
+ console.warn(ex);
+ return [];
+ }
+}
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue
new file mode 100644
index 0000000000..4b145db0ed
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue
@@ -0,0 +1,39 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkWindow
+ ref="uiWindow"
+ :initialWidth="400"
+ :initialHeight="500"
+ :canResize="true"
+ @closed="emit('closed')"
+>
+ <template #header>
+ <i class="ti ti-notes" style="margin-right: 0.5em;"></i> {{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}
+ </template>
+ <MkSpacer>
+ <XRegisterLogs :logs="logs"/>
+ </MkSpacer>
+</MkWindow>
+</template>
+
+<script setup lang="ts">
+import MkWindow from '@/components/MkWindow.vue';
+import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
+
+import { i18n } from '@/i18n.js';
+
+import type { RequestLogItem } from './custom-emojis-manager.impl.js';
+
+defineProps<{
+ logs: RequestLogItem[];
+}>();
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+
+</script>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue
new file mode 100644
index 0000000000..ae43507d66
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue
@@ -0,0 +1,213 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkWindow
+ ref="uiWindow"
+ :initialWidth="400"
+ :initialHeight="500"
+ :canResize="true"
+ @closed="emit('closed')"
+>
+ <template #header>
+ <i class="ti ti-search" style="margin-right: 0.5em;"></i> {{ i18n.ts.search }}
+ </template>
+ <div :class="$style.root">
+ <MkSpacer>
+ <div class="_gaps">
+ <div class="_gaps_s">
+ <MkInput
+ v-model="model.name"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>name</template>
+ </MkInput>
+ <MkInput
+ v-model="model.category"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>category</template>
+ </MkInput>
+ <MkInput
+ v-model="model.aliases"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>aliases</template>
+ </MkInput>
+
+ <MkInput
+ v-model="model.type"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>type</template>
+ </MkInput>
+ <MkInput
+ v-model="model.license"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>license</template>
+ </MkInput>
+ <MkSelect
+ v-model="model.sensitive"
+ >
+ <template #label>sensitive</template>
+ <option :value="null">-</option>
+ <option :value="true">true</option>
+ <option :value="false">false</option>
+ </MkSelect>
+
+ <MkSelect
+ v-model="model.localOnly"
+ >
+ <template #label>localOnly</template>
+ <option :value="null">-</option>
+ <option :value="true">true</option>
+ <option :value="false">false</option>
+ </MkSelect>
+ <MkInput
+ v-model="model.updatedAtFrom"
+ type="date"
+ autocapitalize="off"
+ >
+ <template #label>updatedAt(from)</template>
+ </MkInput>
+ <MkInput
+ v-model="model.updatedAtTo"
+ type="date"
+ autocapitalize="off"
+ >
+ <template #label>updatedAt(to)</template>
+ </MkInput>
+
+ <MkInput
+ v-model="queryRolesText"
+ type="text"
+ readonly
+ autocapitalize="off"
+ @click="onQueryRolesEditClicked"
+ >
+ <template #label>role</template>
+ <template #suffix><i class="ti ti-pencil"></i></template>
+ </MkInput>
+ </div>
+ <MkFolder :spacerMax="8" :spacerMin="8">
+ <template #icon><i class="ti ti-arrows-sort"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
+ <MkSortOrderEditor
+ :baseOrderKeyNames="gridSortOrderKeys"
+ :currentOrders="sortOrders"
+ @update="onSortOrderUpdate"
+ />
+ </MkFolder>
+ </div>
+ </MkSpacer>
+ <div :class="$style.footerActions">
+ <MkButton primary @click="onSearchRequest">
+ {{ i18n.ts.search }}
+ </MkButton>
+ <MkButton @click="onQueryResetButtonClicked">
+ {{ i18n.ts.reset }}
+ </MkButton>
+ </div>
+ </div>
+</MkWindow>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+import MkWindow from '@/components/MkWindow.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
+
+import {
+ gridSortOrderKeys,
+} from './custom-emojis-manager.impl.js';
+
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+import type { EmojiSearchQuery } from './custom-emojis-manager.local.list.vue';
+import type { SortOrder } from '@/components/MkSortOrderEditor.define.js';
+import type { GridSortOrderKey } from './custom-emojis-manager.impl.js';
+
+const props = defineProps<{
+ query: EmojiSearchQuery;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+ (ev: 'queryUpdated', query: EmojiSearchQuery): void;
+ (ev: 'sortOrderUpdated', orders: SortOrder<GridSortOrderKey>[]): void;
+ (ev: 'search'): void;
+}>();
+
+const model = ref<EmojiSearchQuery>(props.query);
+const queryRolesText = computed(() => model.value.roles.map(it => it.name).join(','));
+
+watch(model, () => {
+ emit('queryUpdated', model.value);
+}, { deep: true });
+
+const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
+
+function onSortOrderUpdate(orders: SortOrder<GridSortOrderKey>[]) {
+ sortOrders.value = orders;
+ emit('sortOrderUpdated', orders);
+}
+
+function onSearchRequest() {
+ emit('search');
+}
+
+function onQueryResetButtonClicked() {
+ model.value.name = '';
+ model.value.category = '';
+ model.value.aliases = '';
+ model.value.type = '';
+ model.value.license = '';
+ model.value.sensitive = null;
+ model.value.localOnly = null;
+ model.value.updatedAtFrom = '';
+ model.value.updatedAtTo = '';
+ sortOrders.value = [];
+}
+
+async function onQueryRolesEditClicked() {
+ const result = await os.selectRole({
+ initialRoleIds: model.value.roles.map(it => it.id),
+ title: i18n.ts._customEmojisManager._local._list.dialogSelectRoleTitle,
+ publicOnly: true,
+ });
+ if (result.canceled) {
+ return;
+ }
+
+ model.value.roles = result.result;
+}
+</script>
+
+<style module>
+.root {
+ position: relative;
+}
+
+.footerActions {
+ position: sticky;
+ bottom: 0;
+ padding: var(--MI-margin);
+ background-color: var(--MI_THEME-bg);
+ display: flex;
+ gap: 8px;
+ z-index: 1;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
new file mode 100644
index 0000000000..c4ea3b93e3
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
@@ -0,0 +1,660 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+ <template #header>
+ <MkPageHeader :overridePageMetadata="headerPageMetadata" :actions="headerActions"/>
+ </template>
+ <template #default>
+ <div class="_gaps" :class="$style.main">
+ <component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/>
+ <template v-else>
+ <div v-if="gridItems.length === 0" style="text-align: center">
+ {{ i18n.ts._customEmojisManager._local._list.emojisNothing }}
+ </div>
+
+ <template v-else>
+ <div :class="$style.grid">
+ <MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/>
+ </div>
+ </template>
+ </template>
+ </div>
+ </template>
+
+ <template #footer>
+ <div v-if="gridItems.length > 0" :class="$style.footer">
+ <div :class="$style.left">
+ <MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked">
+ {{ i18n.ts.delete }} ({{ deleteItemsCount }})
+ </MkButton>
+ </div>
+
+ <div :class="$style.center">
+ <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
+ </div>
+
+ <div :class="$style.right">
+ <MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked">
+ {{ i18n.ts.update }} ({{ updatedItemsCount }})
+ </MkButton>
+ <MkButton @click="onGridResetButtonClicked">{{ i18n.ts.reset }}</MkButton>
+ </div>
+ </div>
+ </template>
+</MkStickyContainer>
+</template>
+
+<script lang="ts">
+import type { SortOrder } from '@/components/MkSortOrderEditor.define.js';
+import type { GridSortOrderKey } from './custom-emojis-manager.impl.js';
+
+export type EmojiSearchQuery = {
+ name: string | null;
+ category: string | null;
+ aliases: string | null;
+ type: string | null;
+ license: string | null;
+ updatedAtFrom: string | null;
+ updatedAtTo: string | null;
+ sensitive: string | null;
+ localOnly: string | null;
+ roles: { id: string, name: string }[];
+ sortOrders: SortOrder<GridSortOrderKey>[];
+ limit: number;
+};
+</script>
+
+<script setup lang="ts">
+import { computed, defineAsyncComponent, onMounted, ref, nextTick, useCssModule } from 'vue';
+import * as Misskey from 'misskey-js';
+import * as os from '@/os.js';
+import {
+ emptyStrToEmptyArray,
+ emptyStrToNull,
+ emptyStrToUndefined,
+ RequestLogItem,
+ roleIdsParser,
+} from '@/pages/admin/custom-emojis-manager.impl.js';
+import MkGrid from '@/components/grid/MkGrid.vue';
+import { i18n } from '@/i18n.js';
+import MkButton from '@/components/MkButton.vue';
+import { validators } from '@/components/grid/cell-validators.js';
+import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import MkPagingButtons from '@/components/MkPagingButtons.vue';
+import { GridSetting } from '@/components/grid/grid.js';
+import { selectFile } from '@/scripts/select-file.js';
+import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
+import { useLoading } from "@/components/hook/useLoading.js";
+
+type GridItem = {
+ checked: boolean;
+ id: string;
+ url: string;
+ name: string;
+ host: string;
+ category: string;
+ aliases: string;
+ license: string;
+ isSensitive: boolean;
+ localOnly: boolean;
+ roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
+ fileId?: string;
+ updatedAt: string | null;
+ publicUrl?: string | null;
+ originalUrl?: string | null;
+ type: string | null;
+}
+
+function setupGrid(): GridSetting {
+ const $style = useCssModule();
+
+ const required = validators.required();
+ const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
+ const unique = validators.unique();
+ return {
+ root: {
+ noOverflowStyle: true,
+ rounded: false,
+ outerBorder: false,
+ },
+ row: {
+ showNumber: true,
+ selectable: true,
+ // グリッドの行数をあらかじめ100行確保する
+ minimumDefinitionCount: 100,
+ styleRules: [
+ {
+ // 初期値から変わっていたら背景色を変更
+ condition: ({ row }) => JSON.stringify(gridItems.value[row.index]) !== JSON.stringify(originGridItems.value[row.index]),
+ applyStyle: { className: $style.changedRow },
+ },
+ {
+ // バリデーションに引っかかっていたら背景色を変更
+ condition: ({ cells }) => cells.some(it => !it.violation.valid),
+ applyStyle: { className: $style.violationRow },
+ },
+ ],
+ // 行のコンテキストメニュー設定
+ contextMenuFactory: (row, context) => {
+ return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.copySelectionRows,
+ icon: 'ti ti-copy',
+ action: () => copyGridDataToClipboard(gridItems, context),
+ },
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._local._list.markAsDeleteTargetRows,
+ icon: 'ti ti-trash',
+ action: () => {
+ for (const rangedRow of context.rangedRows) {
+ gridItems.value[rangedRow.index].checked = true;
+ }
+ },
+ },
+ ];
+ },
+ events: {
+ delete(rows) {
+ // 行削除時は元データの行を消さず、削除対象としてマークするのみにする
+ for (const row of rows) {
+ gridItems.value[row.index].checked = true;
+ }
+ },
+ },
+ },
+ cols: [
+ { bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
+ {
+ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required],
+ async customValueEditor(row, col, value, cellElement) {
+ const file = await selectFile(cellElement);
+ gridItems.value[row.index].url = file.url;
+ gridItems.value[row.index].fileId = file.id;
+
+ return file.url;
+ },
+ },
+ {
+ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140,
+ validators: [required, regex, unique],
+ },
+ { bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
+ { bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 },
+ { bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 },
+ { bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
+ { bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
+ {
+ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140,
+ valueTransformer(row) {
+ // バックエンドからからはIDと名前のペア配列で受け取るが、表示にIDがあると煩雑なので名前だけにする
+ return gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction
+ .map((it) => it.name)
+ .join(',');
+ },
+ async customValueEditor(row) {
+ // ID直記入は体験的に最悪なのでモーダルを使って入力する
+ const current = gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction;
+ const result = await os.selectRole({
+ initialRoleIds: current.map(it => it.id),
+ title: i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction,
+ infoMessage: i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription,
+ publicOnly: true,
+ });
+ if (result.canceled) {
+ return current;
+ }
+
+ const transform = result.result.map(it => ({ id: it.id, name: it.name }));
+ gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = transform;
+
+ return transform;
+ },
+ events: {
+ paste: roleIdsParser,
+ delete(cell) {
+ // デフォルトはundefinedになるが、このプロパティは空配列にしたい
+ gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = [];
+ },
+ },
+ },
+ { bindTo: 'type', type: 'text', editable: false, width: 90 },
+ { bindTo: 'updatedAt', type: 'text', editable: false, width: 'auto' },
+ { bindTo: 'publicUrl', type: 'text', editable: false, width: 180 },
+ { bindTo: 'originalUrl', type: 'text', editable: false, width: 180 },
+ ],
+ cells: {
+ // セルのコンテキストメニュー設定
+ contextMenuFactory(col, row, value, context) {
+ return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.copySelectionRanges,
+ icon: 'ti ti-copy',
+ action: () => {
+ return copyGridDataToClipboard(gridItems, context);
+ },
+ },
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.deleteSelectionRanges,
+ icon: 'ti ti-trash',
+ action: () => {
+ removeDataFromGrid(context, (cell) => {
+ gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
+ });
+ },
+ },
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._local._list.markAsDeleteTargetRanges,
+ icon: 'ti ti-trash',
+ action: () => {
+ for (const rowIdx of [...new Set(context.rangedCells.map(it => it.row.index)).values()]) {
+ gridItems.value[rowIdx].checked = true;
+ }
+ },
+ },
+ ];
+ },
+ },
+ };
+}
+
+const loadingHandler = useLoading();
+
+const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
+const allPages = ref<number>(0);
+const currentPage = ref<number>(0);
+
+const searchQuery = ref<EmojiSearchQuery>({
+ name: null,
+ category: null,
+ aliases: null,
+ type: null,
+ license: null,
+ updatedAtFrom: null,
+ updatedAtTo: null,
+ sensitive: null,
+ localOnly: null,
+ roles: [],
+ sortOrders: [],
+ limit: 25,
+});
+let searchWindowOpening = false;
+
+const previousQuery = ref<string | undefined>(undefined);
+const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
+const requestLogs = ref<RequestLogItem[]>([]);
+
+const gridItems = ref<GridItem[]>([]);
+const originGridItems = ref<GridItem[]>([]);
+const updateButtonDisabled = ref<boolean>(false);
+
+const updatedItemsCount = computed(() => {
+ return gridItems.value.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(originGridItems.value[idx])).length;
+});
+const deleteItemsCount = computed(() => gridItems.value.filter(it => it.checked).length);
+
+async function onUpdateButtonClicked() {
+ const _items = gridItems.value;
+ const _originItems = originGridItems.value;
+ if (_items.length !== _originItems.length) {
+ throw new Error('The number of items has been changed. Please refresh the page and try again.');
+ }
+
+ const updatedItems = _items.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(_originItems[idx]));
+ if (updatedItems.length === 0) {
+ await os.alert({
+ type: 'info',
+ text: i18n.ts._customEmojisManager._local._list.alertUpdateEmojisNothingDescription,
+ });
+ return;
+ }
+
+ const { canceled } = await os.confirm({
+ type: 'info',
+ text: i18n.tsx._customEmojisManager._local._list.confirmUpdateEmojisDescription({ count: updatedItems.length }),
+ });
+ if (canceled) {
+ return;
+ }
+
+ const action = () => {
+ return updatedItems.map(item =>
+ misskeyApi(
+ 'admin/emoji/update',
+ {
+ // eslint-disable-next-line
+ id: item.id!,
+ name: item.name,
+ category: emptyStrToNull(item.category),
+ aliases: emptyStrToEmptyArray(item.aliases),
+ license: emptyStrToNull(item.license),
+ isSensitive: item.isSensitive,
+ localOnly: item.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id),
+ fileId: item.fileId,
+ })
+ .then(() => ({ item, success: true, err: undefined }))
+ .catch(err => ({ item, success: false, err })),
+ );
+ };
+
+ const result = await os.promiseDialog(Promise.all(action()));
+ const failedItems = result.filter(it => !it.success);
+
+ if (failedItems.length > 0) {
+ await os.alert({
+ type: 'error',
+ title: i18n.ts.somethingHappened,
+ text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
+ });
+ }
+
+ requestLogs.value = result.map(it => ({
+ failed: !it.success,
+ url: it.item.url,
+ name: it.item.name,
+ error: it.err ? JSON.stringify(it.err) : undefined,
+ }));
+
+ await refreshCustomEmojis();
+}
+
+async function onDeleteButtonClicked() {
+ const _items = gridItems.value;
+ const _originItems = originGridItems.value;
+ if (_items.length !== _originItems.length) {
+ throw new Error('The number of items has been changed. Please refresh the page and try again.');
+ }
+
+ const deleteItems = _items.filter((it) => it.checked);
+ if (deleteItems.length === 0) {
+ await os.alert({
+ type: 'info',
+ text: i18n.ts._customEmojisManager._local._list.alertDeleteEmojisNothingDescription,
+ });
+ return;
+ }
+
+ const { canceled } = await os.confirm({
+ type: 'info',
+ text: i18n.tsx._customEmojisManager._local._list.confirmDeleteEmojisDescription({ count: deleteItems.length }),
+ });
+ if (canceled) {
+ return;
+ }
+
+ async function action() {
+ const deleteIds = deleteItems.map(it => it.id!);
+ await misskeyApi('admin/emoji/delete-bulk', { ids: deleteIds });
+ }
+
+ await os.promiseDialog(
+ action(),
+ );
+}
+
+async function onGridResetButtonClicked() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ title: i18n.ts.resetAreYouSure,
+ text: i18n.ts._customEmojisManager._local._list.confirmResetDescription,
+ });
+
+ if (canceled) return;
+
+ refreshGridItems();
+}
+
+async function onSearchRequest() {
+ await refreshCustomEmojis();
+}
+
+async function onPageChanged(pageNumber: number) {
+ if (updatedItemsCount.value > 0) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ title: i18n.ts._customEmojisManager._local._list.confirmMovePage,
+ text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption,
+ });
+ if (canceled) return;
+ }
+
+ currentPage.value = pageNumber;
+ await nextTick();
+ refreshCustomEmojis();
+}
+
+function onGridEvent(event: GridEvent) {
+ switch (event.type) {
+ case 'cell-validation':
+ onGridCellValidation(event);
+ break;
+ case 'cell-value-change':
+ onGridCellValueChange(event);
+ break;
+ }
+}
+
+function onGridCellValidation(event: GridCellValidationEvent) {
+ updateButtonDisabled.value = event.all.filter(it => !it.valid).length > 0;
+}
+
+function onGridCellValueChange(event: GridCellValueChangeEvent) {
+ const { row, column, newValue } = event;
+ if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
+ gridItems.value[row.index][column.setting.bindTo] = newValue;
+ }
+}
+
+async function refreshCustomEmojis() {
+ const limit = searchQuery.value.limit;
+
+ const query: Misskey.entities.V2AdminEmojiListRequest['query'] = {
+ name: emptyStrToUndefined(searchQuery.value.name),
+ type: emptyStrToUndefined(searchQuery.value.type),
+ aliases: emptyStrToUndefined(searchQuery.value.aliases),
+ category: emptyStrToUndefined(searchQuery.value.category),
+ license: emptyStrToUndefined(searchQuery.value.license),
+ isSensitive: searchQuery.value.sensitive ? Boolean(searchQuery.value.sensitive).valueOf() : undefined,
+ localOnly: searchQuery.value.localOnly ? Boolean(searchQuery.value.localOnly).valueOf() : undefined,
+ updatedAtFrom: emptyStrToUndefined(searchQuery.value.updatedAtFrom),
+ updatedAtTo: emptyStrToUndefined(searchQuery.value.updatedAtTo),
+ roleIds: searchQuery.value.roles.map(it => it.id),
+ hostType: 'local',
+ };
+
+ if (JSON.stringify(query) !== previousQuery.value) {
+ currentPage.value = 1;
+ }
+
+ const result = await loadingHandler.scope(() => misskeyApi('v2/admin/emoji/list', {
+ query: query,
+ limit: limit,
+ page: currentPage.value,
+ sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}` as any),
+ }));
+
+ customEmojis.value = result.emojis;
+ allPages.value = result.allPages;
+
+ previousQuery.value = JSON.stringify(query);
+
+ refreshGridItems();
+}
+
+function refreshGridItems() {
+ gridItems.value = customEmojis.value.map(it => ({
+ checked: false,
+ id: it.id,
+ fileId: undefined,
+ url: it.publicUrl,
+ name: it.name,
+ host: it.host ?? '',
+ category: it.category ?? '',
+ aliases: it.aliases.join(','),
+ license: it.license ?? '',
+ isSensitive: it.isSensitive,
+ localOnly: it.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction,
+ updatedAt: it.updatedAt,
+ publicUrl: it.publicUrl,
+ originalUrl: it.originalUrl,
+ type: it.type,
+ }));
+ originGridItems.value = JSON.parse(JSON.stringify(gridItems.value));
+}
+
+onMounted(async () => {
+ await refreshCustomEmojis();
+});
+
+const headerPageMetadata = computed(() => ({
+ title: i18n.ts._customEmojisManager._local.tabTitleList,
+ icon: 'ti ti-icons',
+}));
+
+const headerActions = computed(() => [{
+ icon: 'ti ti-search',
+ text: i18n.ts.search,
+ handler: () => {
+ if (searchWindowOpening) return;
+ searchWindowOpening = true;
+ const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.search.vue')), {
+ query: searchQuery.value,
+ }, {
+ queryUpdated: (query: EmojiSearchQuery) => {
+ searchQuery.value = query;
+ },
+ sortOrderUpdated: (orders: SortOrder<GridSortOrderKey>[]) => {
+ sortOrders.value = orders;
+ },
+ search: () => {
+ onSearchRequest();
+ },
+ closed: () => {
+ dispose();
+ searchWindowOpening = false;
+ },
+ });
+ },
+}, {
+ icon: 'ti ti-list-numbers',
+ text: i18n.ts._customEmojisManager._gridCommon.searchLimit,
+ handler: (ev: MouseEvent) => {
+ async function changeSearchLimit(to: number) {
+ if (updatedItemsCount.value > 0) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ title: i18n.ts._customEmojisManager._local._list.confirmChangeView,
+ text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption,
+ });
+ if (canceled) return;
+ }
+
+ searchQuery.value.limit = to;
+ refreshCustomEmojis();
+ }
+
+ os.popupMenu([{
+ type: 'radioOption',
+ text: '25',
+ active: computed(() => searchQuery.value.limit === 25),
+ action: () => changeSearchLimit(25),
+ }, {
+ type: 'radioOption',
+ text: '50',
+ active: computed(() => searchQuery.value.limit === 50),
+ action: () => changeSearchLimit(50),
+ }, {
+ type: 'radioOption',
+ text: '100',
+ active: computed(() => searchQuery.value.limit === 100),
+ action: () => changeSearchLimit(100),
+ }], ev.currentTarget ?? ev.target);
+ },
+}, {
+ icon: 'ti ti-notes',
+ text: i18n.ts._customEmojisManager._gridCommon.registrationLogs,
+ handler: () => {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.logs.vue')), {
+ logs: requestLogs.value,
+ }, {
+ closed: () => {
+ dispose();
+ },
+ });
+ }
+}]);
+</script>
+
+<style module lang="scss">
+.violationRow {
+ background-color: var(--MI_THEME-infoWarnBg);
+}
+
+.changedRow {
+ background-color: var(--MI_THEME-infoBg);
+}
+
+.editedRow {
+ background-color: var(--MI_THEME-infoBg);
+}
+
+.main {
+ height: calc(100vh - var(--MI-stickyTop) - var(--MI-stickyBottom));
+ overflow: scroll;
+}
+
+.grid {
+ width: max-content;
+ border-bottom: 1px solid var(--MI_THEME-divider);
+}
+
+.footer {
+ background-color: var(--MI_THEME-bg);
+
+ padding: var(--MI-margin);
+ border-top: 1px solid var(--MI_THEME-divider);
+
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 8px;
+
+ & .left {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 8px;
+ }
+
+ & .center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ }
+
+ & .right {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-direction: row;
+ gap: 8px;
+ }
+}
+
+.divider {
+ margin: 8px 0;
+ border-top: solid 0.5px var(--MI_THEME-divider);
+}
+
+</style>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
new file mode 100644
index 0000000000..cc8b625cd5
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
@@ -0,0 +1,481 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_gaps">
+ <MkFolder>
+ <template #icon><i class="ti ti-settings"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._local._register.uploadSettingTitle }}</template>
+ <template #caption>{{ i18n.ts._customEmojisManager._local._register.uploadSettingDescription }}</template>
+
+ <div class="_gaps">
+ <MkSelect v-model="selectedFolderId">
+ <template #label>{{ i18n.ts.uploadFolder }}</template>
+ <option v-for="folder in uploadFolders" :key="folder.id" :value="folder.id">
+ {{ folder.name }}
+ </option>
+ </MkSelect>
+
+ <MkSwitch v-model="keepOriginalUploading">
+ <template #label>{{ i18n.ts.keepOriginalUploading }}</template>
+ <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
+ </MkSwitch>
+
+ <MkSwitch v-model="directoryToCategory">
+ <template #label>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryLabel }}</template>
+ <template #caption>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryCaption }}</template>
+ </MkSwitch>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
+ <template #icon><i class="ti ti-notes"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template>
+ <template #caption>
+ {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
+ </template>
+ <XRegisterLogs :logs="requestLogs"/>
+ </MkFolder>
+
+ <div
+ :class="[$style.uploadBox, [isDragOver ? $style.dragOver : {}]]"
+ @dragover.prevent="isDragOver = true"
+ @dragleave.prevent="isDragOver = false"
+ @drop.prevent.stop="onDrop"
+ >
+ <div style="margin-top: 1em">
+ {{ i18n.ts._customEmojisManager._local._register.emojiInputAreaCaption }}
+ </div>
+ <ul>
+ <li>{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList1 }}</li>
+ <li><a @click.prevent="onFileSelectClicked">{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList2 }}</a></li>
+ <li><a @click.prevent="onDriveSelectClicked">{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList3 }}</a></li>
+ </ul>
+ </div>
+
+ <div v-if="gridItems.length > 0" :class="$style.gridArea">
+ <MkGrid
+ :data="gridItems"
+ :settings="setupGrid()"
+ @event="onGridEvent"
+ />
+ </div>
+
+ <div v-if="gridItems.length > 0" :class="$style.footer">
+ <MkButton primary :disabled="registerButtonDisabled" @click="onRegistryClicked">
+ {{ i18n.ts.registration }}
+ </MkButton>
+ <MkButton @click="onClearClicked">
+ {{ i18n.ts.clear }}
+ </MkButton>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import * as Misskey from 'misskey-js';
+import { onMounted, ref, useCssModule } from 'vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import {
+ emptyStrToEmptyArray,
+ emptyStrToNull,
+ RequestLogItem,
+ roleIdsParser,
+} from '@/pages/admin/custom-emojis-manager.impl.js';
+import MkGrid from '@/components/grid/MkGrid.vue';
+import { i18n } from '@/i18n.js';
+import MkSelect from '@/components/MkSelect.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { defaultStore } from '@/store.js';
+import MkFolder from '@/components/MkFolder.vue';
+import MkButton from '@/components/MkButton.vue';
+import * as os from '@/os.js';
+import { validators } from '@/components/grid/cell-validators.js';
+import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
+import { uploadFile } from '@/scripts/upload.js';
+import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
+import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
+import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
+import { GridSetting } from '@/components/grid/grid.js';
+import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
+import { GridRow } from '@/components/grid/row.js';
+
+const MAXIMUM_EMOJI_REGISTER_COUNT = 100;
+
+type FolderItem = {
+ id?: string;
+ name: string;
+};
+
+type GridItem = {
+ fileId: string;
+ url: string;
+ name: string;
+ host: string;
+ category: string;
+ aliases: string;
+ license: string;
+ isSensitive: boolean;
+ localOnly: boolean;
+ roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
+ type: string | null;
+}
+
+function setupGrid(): GridSetting {
+ const $style = useCssModule();
+
+ const required = validators.required();
+ const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
+ const unique = validators.unique();
+
+ function removeRows(rows: GridRow[]) {
+ const idxes = [...new Set(rows.map(it => it.index))];
+ gridItems.value = gridItems.value.filter((_, i) => !idxes.includes(i));
+ }
+
+ return {
+ row: {
+ showNumber: true,
+ selectable: true,
+ minimumDefinitionCount: 100,
+ styleRules: [
+ {
+ // 1つでもバリデーションエラーがあれば行全体をエラー表示する
+ condition: ({ cells }) => cells.some(it => !it.violation.valid),
+ applyStyle: { className: $style.violationRow },
+ },
+ ],
+ // 行のコンテキストメニュー設定
+ contextMenuFactory: (row, context) => {
+ return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.copySelectionRows,
+ icon: 'ti ti-copy',
+ action: () => copyGridDataToClipboard(gridItems, context),
+ },
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.deleteSelectionRows,
+ icon: 'ti ti-trash',
+ action: () => removeRows(context.rangedRows),
+ },
+ ];
+ },
+ events: {
+ delete(rows) {
+ removeRows(rows);
+ },
+ },
+ },
+ cols: [
+ { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
+ {
+ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140,
+ validators: [required, regex, unique],
+ },
+ { bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
+ { bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 },
+ { bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 },
+ { bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
+ { bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
+ {
+ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140,
+ valueTransformer: (row) => {
+ // バックエンドからからはIDと名前のペア配列で受け取るが、表示にIDがあると煩雑なので名前だけにする
+ return gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction
+ .map((it) => it.name)
+ .join(',');
+ },
+ customValueEditor: async (row) => {
+ // ID直記入は体験的に最悪なのでモーダルを使って入力する
+ const current = gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction;
+ const result = await os.selectRole({
+ initialRoleIds: current.map(it => it.id),
+ title: i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction,
+ infoMessage: i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription,
+ publicOnly: true,
+ });
+ if (result.canceled) {
+ return current;
+ }
+
+ const transform = result.result.map(it => ({ id: it.id, name: it.name }));
+ gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = transform;
+
+ return transform;
+ },
+ events: {
+ paste: roleIdsParser,
+ delete(cell) {
+ // デフォルトはundefinedになるが、このプロパティは空配列にしたい
+ gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = [];
+ },
+ },
+ },
+ { bindTo: 'type', type: 'text', editable: false, width: 90 },
+ ],
+ cells: {
+ // セルのコンテキストメニュー設定
+ contextMenuFactory: (col, row, value, context) => {
+ return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.copySelectionRanges,
+ icon: 'ti ti-copy',
+ action: () => copyGridDataToClipboard(gridItems, context),
+ },
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.deleteSelectionRanges,
+ icon: 'ti ti-trash',
+ action: () => removeRows(context.rangedCells.map(it => it.row)),
+ },
+ ];
+ },
+ },
+ };
+}
+
+const uploadFolders = ref<FolderItem[]>([]);
+const gridItems = ref<GridItem[]>([]);
+const selectedFolderId = ref(defaultStore.state.uploadFolder);
+const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
+const directoryToCategory = ref<boolean>(false);
+const registerButtonDisabled = ref<boolean>(false);
+const requestLogs = ref<RequestLogItem[]>([]);
+const isDragOver = ref<boolean>(false);
+
+async function onRegistryClicked() {
+ const dialogSelection = await os.confirm({
+ type: 'info',
+ text: i18n.tsx._customEmojisManager._local._register.confirmRegisterEmojisDescription({ count: MAXIMUM_EMOJI_REGISTER_COUNT }),
+ });
+
+ if (dialogSelection.canceled) {
+ return;
+ }
+
+ const items = gridItems.value;
+ const upload = () => {
+ return items.slice(0, MAXIMUM_EMOJI_REGISTER_COUNT)
+ .map(item =>
+ misskeyApi(
+ 'admin/emoji/add', {
+ name: item.name,
+ category: emptyStrToNull(item.category),
+ aliases: emptyStrToEmptyArray(item.aliases),
+ license: emptyStrToNull(item.license),
+ isSensitive: item.isSensitive,
+ localOnly: item.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id),
+ fileId: item.fileId!,
+ })
+ .then(() => ({ item, success: true, err: undefined }))
+ .catch(err => ({ item, success: false, err })),
+ );
+ };
+
+ const result = await os.promiseDialog(Promise.all(upload()));
+ const failedItems = result.filter(it => !it.success);
+
+ if (failedItems.length > 0) {
+ await os.alert({
+ type: 'error',
+ title: i18n.ts.somethingHappened,
+ text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
+ });
+ }
+
+ requestLogs.value = result.map(it => ({
+ failed: !it.success,
+ url: it.item.url,
+ name: it.item.name,
+ error: it.err ? JSON.stringify(it.err) : undefined,
+ }));
+
+ // 登録に成功したものは一覧から除く
+ const successItems = result.filter(it => it.success).map(it => it.item);
+ gridItems.value = gridItems.value.filter(it => !successItems.includes(it));
+}
+
+async function onClearClicked() {
+ const result = await os.confirm({
+ type: 'warning',
+ text: i18n.ts._customEmojisManager._local._register.confirmClearEmojisDescription,
+ });
+
+ if (!result.canceled) {
+ gridItems.value = [];
+ }
+}
+
+async function onDrop(ev: DragEvent) {
+ isDragOver.value = false;
+
+ const droppedFiles = await extractDroppedItems(ev).then(it => flattenDroppedFiles(it));
+ const confirm = await os.confirm({
+ type: 'info',
+ text: i18n.tsx._customEmojisManager._local._register.confirmUploadEmojisDescription({ count: droppedFiles.length }),
+ });
+ if (confirm.canceled) {
+ return;
+ }
+
+ const uploadedItems = Array.of<{ droppedFile: DroppedFile, driveFile: Misskey.entities.DriveFile }>();
+ try {
+ uploadedItems.push(
+ ...await os.promiseDialog(
+ Promise.all(
+ droppedFiles.map(async (it) => ({
+ droppedFile: it,
+ driveFile: await uploadFile(
+ it.file,
+ selectedFolderId.value,
+ it.file.name.replace(/\.[^.]+$/, ''),
+ keepOriginalUploading.value,
+ ),
+ }),
+ ),
+ ),
+ () => {
+ },
+ () => {
+ },
+ ),
+ );
+ } catch (err) {
+ // ダイアログは共通部品側で出ているはずなので何もしない
+ return;
+ }
+
+ const items = uploadedItems.map(({ droppedFile, driveFile }) => {
+ const item = fromDriveFile(driveFile);
+ if (directoryToCategory.value) {
+ item.category = droppedFile.path
+ .replace(/^\//, '')
+ .replace(/\/[^/]+$/, '')
+ .replace(droppedFile.file.name, '');
+ }
+ return item;
+ });
+
+ gridItems.value.push(...items);
+}
+
+async function onFileSelectClicked() {
+ const driveFiles = await chooseFileFromPc(
+ true,
+ {
+ uploadFolder: selectedFolderId.value,
+ keepOriginal: keepOriginalUploading.value,
+ // 拡張子は消す
+ nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
+ },
+ );
+
+ gridItems.value.push(...driveFiles.map(fromDriveFile));
+}
+
+async function onDriveSelectClicked() {
+ const driveFiles = await chooseFileFromDrive(true);
+ gridItems.value.push(...driveFiles.map(fromDriveFile));
+}
+
+function onGridEvent(event: GridEvent) {
+ switch (event.type) {
+ case 'cell-validation':
+ onGridCellValidation(event);
+ break;
+ case 'cell-value-change':
+ onGridCellValueChange(event);
+ break;
+ }
+}
+
+function onGridCellValidation(event: GridCellValidationEvent) {
+ registerButtonDisabled.value = event.all.filter(it => !it.valid).length > 0;
+}
+
+function onGridCellValueChange(event: GridCellValueChangeEvent) {
+ const { row, column, newValue } = event;
+ if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
+ gridItems.value[row.index][column.setting.bindTo] = newValue;
+ }
+}
+
+function fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
+ return {
+ fileId: it.id,
+ url: it.url,
+ name: it.name.replace(/(\.[a-zA-Z0-9]+)+$/, ''),
+ host: '',
+ category: '',
+ aliases: '',
+ license: '',
+ isSensitive: it.isSensitive,
+ localOnly: false,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: [],
+ type: it.type,
+ };
+}
+
+async function refreshUploadFolders() {
+ const result = await misskeyApi('drive/folders', {});
+ uploadFolders.value = Array.of<FolderItem>({ name: '-' }, ...result);
+}
+
+onMounted(async () => {
+ await refreshUploadFolders();
+});
+</script>
+
+<style module lang="scss">
+.violationRow {
+ background-color: var(--MI_THEME-infoWarnBg);
+}
+
+.uploadBox {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: auto;
+ border: 0.5px dotted var(--MI_THEME-accentedBg);
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-accentedBg);
+ box-sizing: border-box;
+
+ &.dragOver {
+ cursor: copy;
+ }
+}
+
+.gridArea {
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.footer {
+ background-color: var(--MI_THEME-bg);
+
+ position: sticky;
+ left:0;
+ bottom:0;
+ z-index: 1;
+ // stickyで追従させる都合上、フッター自身でpaddingを持つ必要があるため、親要素で画一的に指定している分をネガティブマージンで相殺している
+ margin-top: calc(var(--MI-margin) * -1);
+ margin-bottom: calc(var(--MI-margin) * -1);
+ padding-top: var(--MI-margin);
+ padding-bottom: var(--MI-margin);
+
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue
new file mode 100644
index 0000000000..6e7e7e53e3
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue
@@ -0,0 +1,35 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+ <template #header>
+ <MkPageHeader v-model:tab="headerTab" :tabs="headerTabs" hideTitle thin/>
+ </template>
+ <XListComponent v-if="headerTab === 'list'" key="localList"/>
+ <MkSpacer v-else key="localRegister">
+ <XRegisterComponent/>
+ </MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { i18n } from '@/i18n.js';
+import XListComponent from '@/pages/admin/custom-emojis-manager.local.list.vue';
+import XRegisterComponent from '@/pages/admin/custom-emojis-manager.local.register.vue';
+
+type PageMode = 'list' | 'register';
+
+const headerTab = ref<PageMode>('list');
+
+const headerTabs = computed(() => [{
+ key: 'list',
+ title: i18n.ts._customEmojisManager._local.tabTitleList,
+}, {
+ key: 'register',
+ title: i18n.ts._customEmojisManager._local.tabTitleRegister,
+}]);
+</script>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue
new file mode 100644
index 0000000000..eef55a9f7e
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue
@@ -0,0 +1,88 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+ <div v-if="logs.length > 0" style="display:flex; flex-direction: column; overflow-y: scroll; gap: 16px;">
+ <MkSwitch v-model="showingSuccessLogs">
+ <template #label>{{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}</template>
+ </MkSwitch>
+ <div>
+ <div v-if="filteredLogs.length > 0">
+ <MkGrid
+ :data="filteredLogs"
+ :settings="setupGrid()"
+ />
+ </div>
+ <div v-else>
+ {{ i18n.ts._customEmojisManager._logs.failureLogNothing }}
+ </div>
+ </div>
+ </div>
+ <div v-else>
+ {{ i18n.ts._customEmojisManager._logs.logNothing }}
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, toRefs } from 'vue';
+import { i18n } from '@/i18n.js';
+import MkGrid from '@/components/grid/MkGrid.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
+
+import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
+import type { GridSetting } from '@/components/grid/grid.js';
+
+function setupGrid(): GridSetting {
+ return {
+ row: {
+ showNumber: false,
+ selectable: false,
+ contextMenuFactory: (row, context) => {
+ return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.copySelectionRows,
+ icon: 'ti ti-copy',
+ action: () => copyGridDataToClipboard(logs, context),
+ },
+ ];
+ },
+ },
+ cols: [
+ { bindTo: 'failed', title: 'failed', type: 'boolean', editable: false, width: 50 },
+ { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
+ { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 140 },
+ { bindTo: 'error', title: 'log', type: 'text', editable: false, width: 'auto' },
+ ],
+ cells: {
+ contextMenuFactory: (col, row, value, context) => {
+ return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._gridCommon.copySelectionRanges,
+ icon: 'ti ti-copy',
+ action: () => copyGridDataToClipboard(logs, context),
+ },
+ ];
+ },
+ },
+ };
+}
+
+const props = defineProps<{
+ logs: RequestLogItem[];
+}>();
+
+const { logs } = toRefs(props);
+const showingSuccessLogs = ref<boolean>(false);
+
+const filteredLogs = computed(() => {
+ const forceShowing = showingSuccessLogs.value;
+ return logs.value.filter((log) => forceShowing || log.failed);
+});
+</script>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
new file mode 100644
index 0000000000..eecf8d7390
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
@@ -0,0 +1,503 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+ <template #default>
+ <div :class="$style.root" class="_gaps">
+ <MkFolder>
+ <template #icon><i class="ti ti-search"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchSettings }}</template>
+ <template #caption>
+ {{ i18n.ts._customEmojisManager._gridCommon.searchSettingCaption }}
+ </template>
+
+ <div class="_gaps">
+ <div :class="[[spMode ? $style.searchAreaSp : $style.searchArea]]">
+ <MkInput
+ v-model="queryName"
+ type="search"
+ autocapitalize="off"
+ :class="[$style.col1, $style.row1]"
+ @enter="onSearchRequest"
+ >
+ <template #label>name</template>
+ </MkInput>
+ <MkInput
+ v-model="queryHost"
+ type="search"
+ autocapitalize="off"
+ :class="[$style.col2, $style.row1]"
+ @enter="onSearchRequest"
+ >
+ <template #label>host</template>
+ </MkInput>
+ <MkInput
+ v-model="queryLicense"
+ type="search"
+ autocapitalize="off"
+ :class="[$style.col3, $style.row1]"
+ @enter="onSearchRequest"
+ >
+ <template #label>license</template>
+ </MkInput>
+
+ <MkInput
+ v-model="queryUri"
+ type="search"
+ autocapitalize="off"
+ :class="[$style.col1, $style.row2]"
+ @enter="onSearchRequest"
+ >
+ <template #label>uri</template>
+ </MkInput>
+ <MkInput
+ v-model="queryPublicUrl"
+ type="search"
+ autocapitalize="off"
+ :class="[$style.col2, $style.row2]"
+ @enter="onSearchRequest"
+ >
+ <template #label>publicUrl</template>
+ </MkInput>
+ </div>
+
+ <hr>
+
+ <MkFolder :spacerMax="8" :spacerMin="8">
+ <template #icon><i class="ti ti-arrows-sort"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
+ <MkSortOrderEditor
+ :baseOrderKeyNames="gridSortOrderKeys"
+ :currentOrders="sortOrders"
+ @update="onSortOrderUpdate"
+ />
+ </MkFolder>
+
+ <MkInput
+ v-model="queryLimit"
+ type="number"
+ :max="100"
+ >
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchLimit }}</template>
+ </MkInput>
+
+ <div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
+ <MkButton primary @click="onSearchRequest">
+ {{ i18n.ts.search }}
+ </MkButton>
+ <MkButton @click="onQueryResetButtonClicked">
+ {{ i18n.ts.reset }}
+ </MkButton>
+ </div>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
+ <template #icon><i class="ti ti-notes"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template>
+ <template #caption>
+ {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
+ </template>
+ <XRegisterLogs :logs="requestLogs"/>
+ </MkFolder>
+
+ <component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/>
+ <template v-else>
+ <div v-if="gridItems.length === 0" style="text-align: center">
+ {{ i18n.ts._customEmojisManager._local._list.emojisNothing }}
+ </div>
+
+ <template v-else>
+ <div v-if="gridItems.length > 0" :class="$style.gridArea">
+ <MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/>
+ </div>
+
+ <div :class="$style.footer">
+ <div>
+ <!-- レイアウト調整用のスペース -->
+ </div>
+
+ <div :class="$style.center">
+ <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
+ </div>
+
+ <div :class="$style.right">
+ <MkButton primary @click="onImportClicked">
+ {{
+ i18n.ts._customEmojisManager._remote.importEmojisButton
+ }} ({{ checkedItemsCount }})
+ </MkButton>
+ </div>
+ </div>
+ </template>
+ </template>
+ </div>
+ </template>
+</MkStickyContainer>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, useCssModule } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkGrid from '@/components/grid/MkGrid.vue';
+import {
+ emptyStrToUndefined,
+ GridSortOrderKey,
+ gridSortOrderKeys,
+ RequestLogItem,
+} from '@/pages/admin/custom-emojis-manager.impl.js';
+import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
+import MkFolder from '@/components/MkFolder.vue';
+import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
+import * as os from '@/os.js';
+import { GridSetting } from '@/components/grid/grid.js';
+import { deviceKind } from '@/scripts/device-kind.js';
+import MkPagingButtons from '@/components/MkPagingButtons.vue';
+import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
+import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
+import { useLoading } from '@/components/hook/useLoading.js';
+
+type GridItem = {
+ checked: boolean;
+ id: string;
+ url: string;
+ name: string;
+ host: string;
+}
+
+function setupGrid(): GridSetting {
+ const $style = useCssModule();
+
+ return {
+ row: {
+ // グリッドの行数をあらかじめ100行確保する
+ minimumDefinitionCount: 100,
+ styleRules: [
+ {
+ // チェックされたら背景色を変える
+ condition: ({ row }) => gridItems.value[row.index].checked,
+ applyStyle: { className: $style.changedRow },
+ },
+ ],
+ contextMenuFactory: (row, context) => {
+ return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._remote.importSelectionRows,
+ icon: 'ti ti-download',
+ action: async () => {
+ const targets = context.rangedRows.map(it => gridItems.value[it.index]);
+ await importEmojis(targets);
+ },
+ },
+ ];
+ },
+ },
+ cols: [
+ { bindTo: 'checked', icon: 'ti-download', type: 'boolean', editable: true, width: 34 },
+ { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
+ { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' },
+ { bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' },
+ { bindTo: 'license', title: 'license', type: 'text', editable: false, width: 200 },
+ { bindTo: 'uri', title: 'uri', type: 'text', editable: false, width: 'auto' },
+ { bindTo: 'publicUrl', title: 'publicUrl', type: 'text', editable: false, width: 'auto' },
+ ],
+ cells: {
+ contextMenuFactory: (col, row, value, context) => {
+ return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._remote.selectionRowDetail,
+ icon: 'ti ti-info-circle',
+ action: async () => {
+ const target = customEmojis.value[row.index];
+ const { dispose } = os.popup(MkRemoteEmojiEditDialog, {
+ emoji: {
+ id: target.id,
+ name: target.name,
+ host: target.host!,
+ license: target.license,
+ url: target.publicUrl,
+ },
+ }, {
+ done: () => {
+ dispose();
+ },
+ closed: () => {
+ dispose();
+ },
+ });
+ },
+ },
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._remote.importSelectionRangesRows,
+ icon: 'ti ti-download',
+ action: async () => {
+ const targets = context.rangedCells.map(it => gridItems.value[it.row.index]);
+ await importEmojis(targets);
+ },
+ },
+ ];
+ },
+ },
+ };
+}
+
+const loadingHandler = useLoading();
+
+const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
+const allPages = ref<number>(0);
+const currentPage = ref<number>(0);
+
+const queryName = ref<string | null>(null);
+const queryHost = ref<string | null>(null);
+const queryLicense = ref<string | null>(null);
+const queryUri = ref<string | null>(null);
+const queryPublicUrl = ref<string | null>(null);
+const queryLimit = ref<number>(25);
+const previousQuery = ref<string | undefined>(undefined);
+const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
+const requestLogs = ref<RequestLogItem[]>([]);
+
+const gridItems = ref<GridItem[]>([]);
+
+const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind));
+const checkedItemsCount = computed(() => gridItems.value.filter(it => it.checked).length);
+
+function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) {
+ sortOrders.value = _sortOrders;
+}
+
+async function onSearchRequest() {
+ await refreshCustomEmojis();
+}
+
+function onQueryResetButtonClicked() {
+ queryName.value = null;
+ queryHost.value = null;
+ queryLicense.value = null;
+ queryUri.value = null;
+ queryPublicUrl.value = null;
+}
+
+async function onPageChanged(pageNumber: number) {
+ currentPage.value = pageNumber;
+ await refreshCustomEmojis();
+}
+
+async function onImportClicked() {
+ const targets = gridItems.value.filter(it => it.checked);
+ await importEmojis(targets);
+}
+
+function onGridEvent(event: GridEvent) {
+ switch (event.type) {
+ case 'cell-value-change':
+ onGridCellValueChange(event);
+ break;
+ }
+}
+
+function onGridCellValueChange(event: GridCellValueChangeEvent) {
+ const { row, column, newValue } = event;
+ if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
+ gridItems.value[row.index][column.setting.bindTo] = newValue;
+ }
+}
+
+async function importEmojis(targets: GridItem[]) {
+ const confirm = await os.confirm({
+ type: 'info',
+ title: i18n.ts._customEmojisManager._remote.confirmImportEmojisTitle,
+ text: i18n.tsx._customEmojisManager._remote.confirmImportEmojisDescription({ count: targets.length }),
+ });
+
+ if (confirm.canceled) {
+ return;
+ }
+
+ const result = await os.promiseDialog(
+ Promise.all(
+ targets.map(item =>
+ misskeyApi(
+ 'admin/emoji/copy',
+ {
+ emojiId: item.id!,
+ })
+ .then(() => ({ item, success: true, err: undefined }))
+ .catch(err => ({ item, success: false, err })),
+ ),
+ ),
+ );
+ const failedItems = result.filter(it => !it.success);
+
+ if (failedItems.length > 0) {
+ await os.alert({
+ type: 'error',
+ title: i18n.ts.somethingHappened,
+ text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
+ });
+ }
+
+ requestLogs.value = result.map(it => ({
+ failed: !it.success,
+ url: it.item.url,
+ name: it.item.name,
+ error: it.err ? JSON.stringify(it.err) : undefined,
+ }));
+
+ await refreshCustomEmojis();
+}
+
+async function refreshCustomEmojis() {
+ const query: Misskey.entities.V2AdminEmojiListRequest['query'] = {
+ name: emptyStrToUndefined(queryName.value),
+ host: emptyStrToUndefined(queryHost.value),
+ license: emptyStrToUndefined(queryLicense.value),
+ uri: emptyStrToUndefined(queryUri.value),
+ publicUrl: emptyStrToUndefined(queryPublicUrl.value),
+ hostType: 'remote',
+ };
+
+ if (JSON.stringify(query) !== previousQuery.value) {
+ currentPage.value = 1;
+ }
+
+ const result = await loadingHandler.scope(() => misskeyApi('v2/admin/emoji/list', {
+ limit: queryLimit.value,
+ query: query,
+ page: currentPage.value,
+ sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}`) as never[],
+ }));
+
+ customEmojis.value = result.emojis;
+ allPages.value = result.allPages;
+ previousQuery.value = JSON.stringify(query);
+ gridItems.value = customEmojis.value.map(it => ({
+ checked: false,
+ id: it.id,
+ url: it.publicUrl,
+ name: it.name,
+ license: it.license,
+ host: it.host!,
+ }));
+}
+
+onMounted(async () => {
+ await refreshCustomEmojis();
+});
+</script>
+
+<style module lang="scss">
+.row1 {
+ grid-row: 1 / 2;
+}
+
+.row2 {
+ grid-row: 2 / 3;
+}
+
+.col1 {
+ grid-column: 1 / 2;
+}
+
+.col2 {
+ grid-column: 2 / 3;
+}
+
+.col3 {
+ grid-column: 3 / 4;
+}
+
+.root {
+ padding: 16px;
+}
+
+.changedRow {
+ background-color: var(--MI_THEME-infoBg);
+}
+
+.searchArea {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 16px;
+}
+
+.searchButtons {
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 8px;
+}
+
+.searchButtonsSp {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+}
+
+.searchAreaSp {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.gridArea {
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.pages {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ button {
+ background-color: var(--MI_THEME-buttonBg);
+ border-radius: 9999px;
+ border: none;
+ margin: 0 4px;
+ padding: 8px;
+ }
+}
+
+.footer {
+ background-color: var(--MI_THEME-bg);
+
+ position: sticky;
+ left:0;
+ bottom:0;
+ z-index: 1;
+ // stickyで追従させる都合上、フッター自身でpaddingを持つ必要があるため、親要素で画一的に指定している分をネガティブマージンで相殺している
+ margin-top: calc(var(--MI-margin) * -1);
+ margin-bottom: calc(var(--MI-margin) * -1);
+ padding-top: var(--MI-margin);
+ padding-bottom: var(--MI-margin);
+
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 8px;
+
+ & .center {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ & .right {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ }
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts
new file mode 100644
index 0000000000..f62304277a
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts
@@ -0,0 +1,160 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { delay, http, HttpResponse } from 'msw';
+import { StoryObj } from '@storybook/vue3';
+import { entities } from 'misskey-js';
+import { commonHandlers } from '../../../.storybook/mocks.js';
+import { emoji } from '../../../.storybook/fakes.js';
+import { fakeId } from '../../../.storybook/fake-utils.js';
+import custom_emojis_manager2 from './custom-emojis-manager2.vue';
+
+function createRender(params: {
+ emojis: entities.EmojiDetailedAdmin[];
+}) {
+ const storedEmojis: entities.EmojiDetailedAdmin[] = [...params.emojis];
+ const storedDriveFiles: entities.DriveFile[] = [];
+
+ return {
+ render(args) {
+ return {
+ components: {
+ custom_emojis_manager2,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<custom_emojis_manager2 v-bind="props" />',
+ };
+ },
+ args: {
+
+ },
+ parameters: {
+ layout: 'fullscreen',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ http.post('/api/v2/admin/emoji/list', async ({ request }) => {
+ await delay(100);
+
+ const bodyStream = request.body as ReadableStream;
+ const body = await new Response(bodyStream).json() as entities.V2AdminEmojiListRequest;
+
+ const emojis = storedEmojis;
+ const limit = body.limit ?? 10;
+ const page = body.page ?? 1;
+ const result = emojis.slice((page - 1) * limit, page * limit);
+
+ return HttpResponse.json({
+ emojis: result,
+ count: Math.min(emojis.length, limit),
+ allCount: emojis.length,
+ allPages: Math.ceil(emojis.length / limit),
+ });
+ }),
+ http.post('/api/drive/folders', () => {
+ return HttpResponse.json([]);
+ }),
+ http.post('/api/drive/files', () => {
+ return HttpResponse.json(storedDriveFiles);
+ }),
+ http.post('/api/drive/files/create', async ({ request }) => {
+ const data = await request.formData();
+ const file = data.get('file');
+ if (!file || !(file instanceof File)) {
+ return HttpResponse.json({ error: 'file is required' }, {
+ status: 400,
+ });
+ }
+
+ // FIXME: ファイルのバイナリに0xEF 0xBF 0xBDが混入してしまい、うまく画像ファイルとして表示できない問題がある
+ const base64 = await new Promise<string>((resolve) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result as string);
+ };
+ reader.readAsDataURL(new Blob([file], { type: 'image/webp' }));
+ });
+
+ const driveFile: entities.DriveFile = {
+ id: fakeId(file.name),
+ createdAt: new Date().toISOString(),
+ name: file.name,
+ type: file.type,
+ md5: '',
+ size: file.size,
+ isSensitive: false,
+ blurhash: null,
+ properties: {},
+ url: base64,
+ thumbnailUrl: null,
+ comment: null,
+ folderId: null,
+ folder: null,
+ userId: null,
+ user: null,
+ };
+
+ storedDriveFiles.push(driveFile);
+
+ return HttpResponse.json(driveFile);
+ }),
+ http.post('api/admin/emoji/add', async ({ request }) => {
+ await delay(100);
+
+ const bodyStream = request.body as ReadableStream;
+ const body = await new Response(bodyStream).json() as entities.AdminEmojiAddRequest;
+
+ const fileId = body.fileId;
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const file = storedDriveFiles.find(f => f.id === fileId)!;
+
+ const em = emoji({
+ id: fakeId(file.name),
+ name: body.name,
+ publicUrl: file.url,
+ originalUrl: file.url,
+ type: file.type,
+ aliases: body.aliases,
+ category: body.category ?? undefined,
+ license: body.license ?? undefined,
+ localOnly: body.localOnly,
+ isSensitive: body.isSensitive,
+ });
+ storedEmojis.push(em);
+
+ return HttpResponse.json(null);
+ }),
+ ],
+ },
+ },
+ } satisfies StoryObj<typeof custom_emojis_manager2>;
+}
+
+export const Default = createRender({
+ emojis: [],
+});
+
+export const List10 = createRender({
+ emojis: Array.from({ length: 10 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())),
+});
+
+export const List100 = createRender({
+ emojis: Array.from({ length: 100 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())),
+});
+
+export const List1000 = createRender({
+ emojis: Array.from({ length: 1000 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())),
+});
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
new file mode 100644
index 0000000000..fb930064ff
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
@@ -0,0 +1,51 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+ <MkStickyContainer>
+ <template #header>
+ <MkPageHeader v-model:tab="headerTab" :tabs="headerTabs"/>
+ </template>
+ <XGridLocalComponent v-if="headerTab === 'local'" :class="$style.local"/>
+ <XGridRemoteComponent v-else/>
+ </MkStickyContainer>
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import XGridLocalComponent from '@/pages/admin/custom-emojis-manager.local.vue';
+import XGridRemoteComponent from '@/pages/admin/custom-emojis-manager.remote.vue';
+import MkPageHeader from '@/components/global/MkPageHeader.vue';
+import MkStickyContainer from '@/components/global/MkStickyContainer.vue';
+
+type PageMode = 'local' | 'remote';
+
+const headerTab = ref<PageMode>('local');
+
+const headerTabs = computed(() => [{
+ key: 'local',
+ title: i18n.ts.local,
+}, {
+ key: 'remote',
+ title: i18n.ts.remote,
+}]);
+
+definePageMetadata(computed(() => ({
+ title: i18n.ts.customEmojis,
+ icon: 'ti ti-icons',
+ needWideArea: true,
+})));
+</script>
+
+<style lang="css" module>
+.local {
+ height: calc(100dvh - var(--MI-stickyTop) - var(--MI-stickyBottom));
+ overflow: clip;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index ef6bbb865b..188678c183 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="federating">{{ i18n.ts.federating }}</option>
<option value="subscribing">{{ i18n.ts.subscribing }}</option>
<option value="publishing">{{ i18n.ts.publishing }}</option>
+ <!-- TODO translate -->
<option value="nsfw">NSFW</option>
<option value="suspended">{{ i18n.ts.suspended }}</option>
<option value="blocked">{{ i18n.ts.blocked }}</option>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 6cdf0eda7a..cbd0d12dcc 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -35,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
+import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
import MkInfo from '@/components/MkInfo.vue';
import { instance } from '@/instance.js';
import { lookup } from '@/scripts/lookup.js';
@@ -56,7 +57,7 @@ const indexInfo = {
provide('shouldOmitHeaderTitle', false);
-const INFO = ref(indexInfo);
+const INFO = ref<PageMetadata>(indexInfo);
const childInfo = ref<null | PageMetadata>(null);
const narrow = ref(false);
const view = ref(null);
@@ -91,7 +92,7 @@ const ro = new ResizeObserver((entries, observer) => {
narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
-const menuDef = computed(() => [{
+const menuDef = computed<SuperMenuDef[]>(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
@@ -99,7 +100,7 @@ const menuDef = computed(() => [{
text: i18n.ts.lookup,
action: adminLookup,
}, ...(instance.disableRegistration ? [{
- type: 'button',
+ type: 'button' as const,
icon: 'ti ti-user-plus',
text: i18n.ts.createInviteCode,
action: invite,
@@ -137,6 +138,11 @@ const menuDef = computed(() => [{
to: '/admin/emojis',
active: currentPage.value?.route.name === 'emojis',
}, {
+ icon: 'ti ti-icons',
+ text: i18n.ts.customEmojis + '(beta)',
+ to: '/admin/emojis2',
+ active: currentPage.value?.route.name === 'emojis2',
+ }, {
icon: 'ti ti-sparkles',
text: i18n.ts.avatarDecorations,
to: '/admin/avatar-decorations',
@@ -343,12 +349,14 @@ defineExpose({
height: 100%;
> .nav {
+ position: sticky;
+ top: 0;
width: 32%;
max-width: 280px;
box-sizing: border-box;
border-right: solid 0.5px var(--MI_THEME-divider);
overflow: auto;
- height: 100%;
+ height: 100dvh;
}
> .main {
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 37a9cc83e7..9ce6499e2d 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -18,15 +18,27 @@ SPDX-License-Identifier: AGPL-3.0-only
'createAvatarDecoration',
'createSystemWebhook',
'createAbuseReportNotificationRecipient',
+ 'createAccount',
+ 'importCustomEmojis',
+ 'createPromo',
+ 'addRelay',
].includes(log.type),
[$style.logYellow]: [
'markSensitiveDriveFile',
'resetPassword',
+ 'setMandatoryCW',
'suspendRemoteInstance',
'setRemoteInstanceNSFW',
'unsetRemoteInstanceNSFW',
'rejectRemoteInstanceReports',
'acceptRemoteInstanceReports',
+ 'rejectQuotesUser',
+ 'acceptQuotesUser',
+ 'nsfwUser',
+ 'unNsfwUser',
+ 'silenceUser',
+ 'unSilenceUser',
+ 'updateCustomEmojis',
].includes(log.type),
[$style.logRed]: [
'suspend',
@@ -46,15 +58,26 @@ SPDX-License-Identifier: AGPL-3.0-only
'deletePage',
'deleteFlash',
'deleteGalleryPost',
+ 'clearUserFiles',
+ 'clearRemoteFiles',
+ 'clearOwnerlessFiles',
+ 'clearInstanceFiles',
+ 'severFollowRelations',
+ 'removeRelay',
].includes(log.type)
}"
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'approve'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'rejectQuotesUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'acceptQuotesUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'rejectQuotesInstance'">: {{ log.info.host }}</span>
+ <span v-else-if="log.type === 'acceptQuotesInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'decline'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'setMandatoryCW'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-equal-not"></i> {{ log.info.roleName }}</span>
<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
@@ -92,8 +115,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span>
<span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span>
<span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span>
+ <span v-else-if="log.type === 'clearUserFiles'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'nsfwUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'unNsfwUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'silenceUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'unSilenceUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
+ <span v-else-if="log.type === 'createAccount'">: @{{ log.info.userUsername }}</span>
+ <span v-else-if="log.type === 'clearOwnerlessFiles'">: {{ log.info.count }}</span>
+ <span v-else-if="log.type === 'importCustomEmojis'">: {{ log.info.fileName }}</span>
+ <span v-else-if="log.type === 'clearInstanceFiles'">: {{ log.info.host }}</span>
+ <span v-else-if="log.type === 'severFollowRelations'">: {{ log.info.host }}</span>
+ <span v-else-if="log.type === 'createPromo'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
+ <span v-else-if="log.type === 'addRelay'">: {{ log.info.inbox }}</span>
+ <span v-else-if="log.type === 'removeRelay'">: {{ log.info.inbox }}</span>
</template>
- <template #icon>
+ <template v-if="log.user" #icon>
<MkAvatar :user="log.user" :class="$style.avatar"/>
</template>
<template #suffix>
@@ -123,9 +159,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else-if="log.type === 'approve'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
</template>
+ <template v-else-if="log.type === 'setMandatoryCW'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
+ <div :class="$style.diff">
+ <CodeDiff :context="0" :hideHeader="true" :oldString="log.info.oldCW ?? ''" :newString="log.info.newCW ?? ''" maxHeight="150px"/>
+ </div>
+ </template>
<template v-else-if="log.type === 'unsuspend'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
</template>
+ <template v-else-if="log.type === 'rejectQuotesUser'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
+ </template>
+ <template v-else-if="log.type === 'acceptQuotesUser'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
+ </template>
<template v-else-if="log.type === 'updateRole'">
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
@@ -185,6 +233,47 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div>
</template>
+ <template v-else-if="log.type === 'clearUserFiles'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
+ <div>{{ i18n.ts.filesRemoved }}: {{ log.info.count }}</div>
+ </template>
+ <template v-else-if="log.type === 'nsfwUser'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
+ </template>
+ <template v-else-if="log.type === 'unNsfwUser'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
+ </template>
+ <template v-else-if="log.type === 'silenceUser'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
+ </template>
+ <template v-else-if="log.type === 'unSilenceUser'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
+ </template>
+ <template v-else-if="log.type === 'createAccount'">
+ <div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}</MkA></div>
+ </template>
+ <template v-else-if="log.type === 'clearOwnerlessFiles'">
+ <div>{{ i18n.ts.filesRemoved }}: {{ log.info.count }}</div>
+ </template>
+ <template v-else-if="log.type === 'importCustomEmojis'">
+ <div>{{ i18n.ts.fileImported }}: {{ log.info.fileName }}</div>
+ </template>
+ <template v-else-if="log.type === 'clearInstanceFiles'">
+ <div>{{ i18n.ts.host }}: <MkA :to="`/instance-info/${log.info.host}`" class="_link">{{ log.info.host }}</MkA></div>
+ <div>{{ i18n.ts.filesRemoved }}: {{ log.info.count }}</div>
+ </template>
+ <template v-else-if="log.type === 'severFollowRelations'">
+ <div>{{ i18n.ts.host }}: <MkA :to="`/instance-info/${log.info.host}`" class="_link">{{ log.info.host }}</MkA></div>
+ </template>
+ <template v-else-if="log.type === 'createPromo'">
+ <SkFetchNote :noteId="log.info.noteId"/>
+ </template>
+ <template v-else-if="log.type === 'addRelay'">
+ <div>{{ i18n.ts.inboxUrl }}: {{ log.info.inbox }}</div>
+ </template>
+ <template v-else-if="log.type === 'removeRelay'">
+ <div>{{ i18n.ts.inboxUrl }}: {{ log.info.inbox }}</div>
+ </template>
<details>
<summary>raw</summary>
@@ -200,6 +289,7 @@ import { CodeDiff } from 'v-code-diff';
import JSON5 from 'json5';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
+import SkFetchNote from '@/components/SkFetchNote.vue';
const props = defineProps<{
log: Misskey.entities.ModerationLog;
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 5d896db98c..6bab594d36 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -641,7 +641,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="role.policies.avatarDecorationLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
- <MkInput v-model="role.policies.avatarDecorationLimit.value" type="number" :min="0">
+ <MkInput v-model="role.policies.avatarDecorationLimit.value" type="number" :min="0" :max="16" @update:modelValue="updateAvatarDecorationLimit">
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
</MkInput>
<MkRange v-model="role.policies.avatarDecorationLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
@@ -757,6 +757,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import { throttle } from 'throttle-debounce';
+import { ROLE_POLICIES } from '@@/js/const.js';
import RolesEditorFormula from './RolesEditorFormula.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
@@ -767,7 +768,6 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue';
import { i18n } from '@/i18n.js';
-import { ROLE_POLICIES } from '@@/js/const.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/scripts/clone.js';
@@ -793,6 +793,12 @@ for (const ROLE_POLICY of ROLE_POLICIES) {
}
}
+function updateAvatarDecorationLimit(value: string | number) {
+ const numValue = Number(value);
+ const limited = Math.min(16, Math.max(0, numValue));
+ role.value.policies.avatarDecorationLimit.value = limited;
+}
+
const rolePermission = computed({
get: () => role.value.isAdministrator ? 'administrator' : role.value.isModerator ? 'moderator' : 'normal',
set: (val) => {
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 036f18fe0d..f67b1cd582 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -239,7 +239,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])">
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
<template #suffix>{{ policies.avatarDecorationLimit }}</template>
- <MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0">
+ <MkInput v-model="avatarDecorationLimit" type="number" :min="0" :max="16" @update:modelValue="updateAvatarDecorationLimit">
</MkInput>
</MkFolder>
@@ -334,6 +334,17 @@ for (const ROLE_POLICY of ROLE_POLICIES) {
policies[ROLE_POLICY] = instance.policies[ROLE_POLICY];
}
+const avatarDecorationLimit = computed({
+ get: () => Math.min(16, Math.max(0, policies.avatarDecorationLimit)),
+ set: (value) => {
+ policies.avatarDecorationLimit = Math.min(Number(value), 16);
+ },
+});
+
+function updateAvatarDecorationLimit(value: string | number) {
+ avatarDecorationLimit.value = Number(value);
+}
+
function matchQuery(keywords: string[]): boolean {
if (baseRoleQ.value.trim().length === 0) return true;
return keywords.some(keyword => keyword.toLowerCase().includes(baseRoleQ.value.toLowerCase()));
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 68f211de5c..0e9c0a7d38 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -138,6 +138,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
+
+ <MkButton primary @click="genKeys">{{ i18n.ts.genKeys }}</MkButton>
</template>
</div>
</MkFolder>
@@ -159,6 +161,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.enableBotTrending }}<span v-if="otherForm.modifiedStates.enableBotTrending" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.turnOffBotTrending }}</template>
</MkSwitch>
+
+ <MkTextarea v-model="otherForm.state.robotsTxt">
+ <template #label>{{ i18n.ts.robotsTxt }}<span v-if="otherForm.modifiedStates.robotsTxt" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts.robotsTxtDescription }}</template>
+ </MkTextarea>
</div>
</MkFolder>
@@ -369,10 +376,12 @@ const serviceWorkerForm = useForm({
const otherForm = useForm({
enableAchievements: meta.enableAchievements,
enableBotTrending: meta.enableBotTrending,
+ robotsTxt: meta.robotsTxt,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableAchievements: state.enableAchievements,
enableBotTrending: state.enableBotTrending,
+ robotsTxt: state.robotsTxt,
});
fetchInstance(true);
});
@@ -427,6 +436,18 @@ function chooseProxyAccount() {
});
}
+async function genKeys() {
+ if (serviceWorkerForm.savedState.swPrivateKey) {
+ const result = await os.confirm({ type: 'warning', title: i18n.ts._genKeysDialog.title, text: i18n.ts._genKeysDialog.text });
+ if (result.canceled) return;
+ }
+
+ const keys = await os.apiWithDialog('admin/gen-vapid-keys', {});
+
+ serviceWorkerForm.state.swPublicKey = keys.public;
+ serviceWorkerForm.state.swPrivateKey = keys.private;
+}
+
const headerTabs = computed(() => []);
definePageMetadata(() => ({
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index e26a2c827e..c693eed850 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -34,11 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
</div>
<div :class="$style.inputs">
- <MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false">
+ <MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" debounce>
<template #prefix>@</template>
<template #label>{{ i18n.ts.username }}</template>
</MkInput>
- <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'">
+ <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" debounce>
<template #prefix>@</template>
<template #label>{{ i18n.ts.host }}</template>
</MkInput>
diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue
index 30f12a8fb3..1e3bb0de6b 100644
--- a/packages/frontend/src/pages/api-console.vue
+++ b/packages/frontend/src/pages/api-console.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="700">
<div class="_gaps_m">
<div class="_gaps_m">
- <MkInput v-model="endpoint" :datalist="endpoints" @update:modelValue="onEndpointChange()">
+ <MkInput v-model="endpoint" :datalist="endpoints" debounce @update:modelValue="onEndpointChange()">
<template #label>Endpoint</template>
</MkInput>
<MkTextarea v-model="body" code>
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 9cd2546312..fb99379a0a 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkNotes :pagination="featuredPagination"/>
</div>
<div v-else-if="tab === 'search'" key="search">
- <div class="_gaps">
+ <div v-if="notesSearchAvailable" class="_gaps">
<div>
<MkInput v-model="searchQuery" @enter="search()">
<template #prefix><i class="ti ti-search"></i></template>
@@ -54,6 +54,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
</div>
+ <div v-else>
+ <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
+ </div>
</div>
</MkHorizontalSwipe>
</MkSpacer>
@@ -94,6 +97,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { PageHeaderItem } from '@/types/page-header.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { notesSearchAvailable } from '@/scripts/check-permissions.js';
import { miLocalStorage } from '@/local-storage.js';
import { useRouter } from '@/router/supplier.js';
import { deepMerge } from '@/scripts/merge.js';
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index bde1650754..6830c1ace4 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :contentMax="700">
+ <MkSpacer :contentMax="1200">
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
- <div v-if="tab === 'search'" key="search">
+ <div v-if="tab === 'search'" key="search" :class="$style.searchRoot">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<template #prefix><i class="ti ti-search"></i></template>
@@ -27,23 +27,31 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="tab === 'featured'" key="featured">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
- <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
+ <div :class="$style.root">
+ <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
+ </div>
</MkPagination>
</div>
<div v-else-if="tab === 'favorites'" key="favorites">
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
- <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
+ <div :class="$style.root">
+ <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
+ </div>
</MkPagination>
</div>
<div v-else-if="tab === 'following'" key="following">
<MkPagination v-slot="{items}" :pagination="followingPagination">
- <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
+ <div :class="$style.root">
+ <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
+ </div>
</MkPagination>
</div>
<div v-else-if="tab === 'owned'" key="owned">
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="ownedPagination">
- <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
+ <div :class="$style.root">
+ <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
+ </div>
</MkPagination>
</div>
</MkHorizontalSwipe>
@@ -85,6 +93,7 @@ onMounted(() => {
const featuredPagination = {
endpoint: 'channels/featured' as const,
+ limit: 10,
noPaging: true,
};
const favoritesPagination = {
@@ -157,3 +166,17 @@ definePageMetadata(() => ({
icon: 'ti ti-device-tv',
}));
</script>
+
+<style lang="scss" module>
+.searchRoot {
+ width: 100%;
+ max-width: 700px;
+ margin: 0 auto;
+}
+
+.root {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
+ gap: var(--MI-margin);
+}
+</style>
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 716cd9a73f..240f395e04 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -46,9 +46,10 @@ import { clipsCache } from '@/cache.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
-import { getServerContext } from '@/server-context.js';
+import { assertServerContext, serverContext } from '@/server-context.js';
-const CTX_CLIP = getServerContext('clip');
+// contextは非ログイン状態の情報しかないためログイン時は利用できない
+const CTX_CLIP = !$i && assertServerContext(serverContext, 'clip') ? serverContext.clip : null;
const props = defineProps<{
clipId: string,
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 850c1c5eb0..107a0d760c 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{items}">
<div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
- <img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/>
+ <img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.category }}</div>
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{items}">
<div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
- <img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" class="img" :alt="emoji.name"/>
+ <img :src="getProxiedImageUrl(emoji.url, 'emoji')" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.host }}</div>
@@ -78,11 +78,13 @@ import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
+import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSplit from '@/components/form/split.vue';
import { selectFile } from '@/scripts/select-file.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
+import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -161,6 +163,19 @@ const edit = (emoji) => {
});
};
+const detailRemoteEmoji = (emoji) => {
+ const { dispose } = os.popup(MkRemoteEmojiEditDialog, {
+ emoji: emoji,
+ }, {
+ done: () => {
+ dispose();
+ },
+ closed: () => {
+ dispose();
+ },
+ });
+};
+
const importEmoji = (emoji) => {
os.apiWithDialog('admin/emoji/copy', {
emojiId: emoji.id,
@@ -171,13 +186,15 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
os.popupMenu([{
type: 'label',
text: ':' + emoji.name + ':',
- },
- {
+ }, {
+ text: i18n.ts.details,
+ icon: 'ti ti-info-circle',
+ action: () => { detailRemoteEmoji(emoji); },
+ }, {
text: i18n.ts.import,
icon: 'ti ti-plus',
action: () => { importEmoji(emoji); },
- },
- {
+ }, {
text: i18n.ts.delete,
icon: 'ph-trash ph-bold ph-lg',
action: () => {
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 9d74a47ec6..8706dc7047 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
<button class="_button" :class="$style.kvEditBtn" @click="describe()">
<MkKeyValue :class="$style.multiline">
- <template #key>{{ i18n.ts.description }}</template>
+ <template #key>{{ i18n.ts.caption }}</template>
<template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.kvEditIcon"></i></template>
</MkKeyValue>
</button>
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index d3e9ca0dcf..c8e6dfb05a 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -118,7 +118,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
}, { immediate: true });
-const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
+const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null);
async function changeImage(ev: Event) {
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index 2550100a42..0d2c6217d4 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkSpacer :contentMax="1200">
- <MkTab v-model="origin" style="margin-bottom: var(--MI-margin);">
+ <MkTab v-if="instance.federation !== 'none'" v-model="origin" style="margin-bottom: var(--MI-margin);">
<option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
</MkTab>
@@ -69,6 +69,7 @@ import MkUserList from '@/components/MkUserList.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkTab from '@/components/MkTab.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
+import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 96aebb14d2..32a9d30264 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -53,6 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo>
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">{{ i18n.ts.markInstanceAsNSFW }}</MkSwitch>
+ <MkSwitch v-model="rejectQuotes" :disabled="!instance" @update:modelValue="toggleRejectQuotes">{{ i18n.ts.rejectQuotesInstance }}</MkSwitch>
<MkSwitch v-model="rejectReports" :disabled="!instance" @update:modelValue="toggleRejectReports">{{ i18n.ts.rejectReports }}</MkSwitch>
<MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo>
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
@@ -211,6 +212,7 @@ const isSuspended = ref(false);
const isBlocked = ref(false);
const isSilenced = ref(false);
const isNSFW = ref(false);
+const rejectQuotes = ref(false);
const rejectReports = ref(false);
const isMediaSilenced = ref(false);
const faviconUrl = ref<string | null>(null);
@@ -282,6 +284,7 @@ async function fetch(): Promise<void> {
isSilenced.value = instance.value?.isSilenced ?? false;
isNSFW.value = instance.value?.isNSFW ?? false;
rejectReports.value = instance.value?.rejectReports ?? false;
+ rejectQuotes.value = instance.value?.rejectQuotes ?? false;
isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
moderationNote.value = instance.value?.moderationNote ?? '';
@@ -347,6 +350,15 @@ async function toggleRejectReports(): Promise<void> {
});
}
+async function toggleRejectQuotes(): Promise<void> {
+ if (!iAmModerator) return;
+ if (!instance.value) throw new Error('No instance?');
+ await misskeyApi('admin/federation/update-instance', {
+ host: instance.value.host,
+ rejectQuotes: rejectQuotes.value,
+ });
+}
+
function refreshMetadata(): void {
if (!iAmModerator) return;
if (!instance.value) throw new Error('No instance?');
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index e85d2c29c1..ab060587c5 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -59,18 +59,18 @@ async function onAccept(token: string) {
name: props.name,
iconUrl: props.icon,
permission: _permissions.value,
- }, token).catch(() => {
+ }, token).then(() => {
+ if (props.callback && props.callback !== '') {
+ const cbUrl = new URL(props.callback);
+ if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
+ cbUrl.searchParams.set('session', props.session);
+ location.href = cbUrl.toString();
+ } else {
+ authRoot.value?.showUI('success');
+ }
+ }).catch(() => {
authRoot.value?.showUI('failed');
});
-
- if (props.callback && props.callback !== '') {
- const cbUrl = new URL(props.callback);
- if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
- cbUrl.searchParams.set('session', props.session);
- location.href = cbUrl.toString();
- } else {
- authRoot.value?.showUI('success');
- }
}
function onDeny() {
@@ -117,5 +117,6 @@ definePageMetadata(() => ({
border-radius: var(--MI-radius);
background-color: var(--MI_THEME-panel);
overflow-x: scroll;
+ white-space: nowrap;
}
</style>
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 737b0eea4c..5214ca4849 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="_margin _gaps_s">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
+ <SkErrorList :errors="note.processErrors"/>
<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
</div>
<div v-if="clips && clips.length > 0" class="_margin">
@@ -50,6 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
import type { Paging } from '@/components/MkPagination.vue';
import MkNotes from '@/components/MkNotes.vue';
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
@@ -59,16 +61,19 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkClipPreview from '@/components/MkClipPreview.vue';
+import SkErrorList from '@/components/SkErrorList.vue';
import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
-import { getServerContext } from '@/server-context.js';
+import { serverContext, assertServerContext } from '@/server-context.js';
+import { $i } from '@/account.js';
-const CTX_NOTE = getServerContext('note');
+// contextは非ログイン状態の情報しかないためログイン時は利用できない
+const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
const MkNoteDetailed = defineAsyncComponent(() =>
- (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteDetailed.vue') :
- (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNoteDetailed.vue') :
- null
+ (defaultStore.state.noteDesign === 'misskey')
+ ? import('@/components/MkNoteDetailed.vue')
+ : import('@/components/SkNoteDetailed.vue'),
);
const props = defineProps<{
@@ -146,7 +151,12 @@ function fetchNote() {
}).catch(err => {
if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
pleaseLogin({
+ path: '/',
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
+ openOnRemote: {
+ type: 'lookup',
+ url: `https://${host}/notes/${props.noteId}`,
+ },
});
}
error.value = err;
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index ac9f3e7401..8597654375 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -96,7 +96,7 @@ const summary = ref<string | null>(null);
const name = ref(Date.now().toString());
const eyeCatchingImage = ref<Misskey.entities.DriveFile | null>(null);
const eyeCatchingImageId = ref<string | null>(null);
-const font = ref('sans-serif');
+const font = ref<'sans-serif' | 'serif'>('sans-serif');
const content = ref<Misskey.entities.Page['content']>([]);
const alignCenter = ref(false);
const hideTitleWhenPinned = ref(false);
@@ -113,7 +113,7 @@ watch(eyeCatchingImageId, async () => {
}
});
-function getSaveOptions() {
+function getSaveOptions(): Misskey.entities.PagesCreateRequest {
return {
title: title.value.trim(),
name: name.value.trim(),
@@ -128,80 +128,69 @@ function getSaveOptions() {
};
}
-function save() {
+async function save() {
const options = getSaveOptions();
- const onError = err => {
- if (err.id === '3d81ceae-475f-4600-b2a8-2bc116157532') {
- if (err.info.param === 'name') {
- os.alert({
- type: 'error',
- title: i18n.ts._pages.invalidNameTitle,
- text: i18n.ts._pages.invalidNameText,
- });
- }
- } else if (err.code === 'NAME_ALREADY_EXISTS') {
- os.alert({
- type: 'error',
+ if (pageId.value) {
+ const updateOptions: Misskey.entities.PagesUpdateRequest = {
+ pageId: pageId.value,
+ ...options,
+ };
+
+ await os.apiWithDialog('pages/update', updateOptions, undefined, {
+ '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab': {
+ title: i18n.ts.somethingHappened,
text: i18n.ts._pages.nameAlreadyExists,
- });
- }
- };
+ },
+ });
- if (pageId.value) {
- options.pageId = pageId.value;
- misskeyApi('pages/update', options)
- .then(page => {
- currentName.value = name.value.trim();
- os.alert({
- type: 'success',
- text: i18n.ts._pages.updated,
- });
- }).catch(onError);
+ currentName.value = name.value.trim();
} else {
- misskeyApi('pages/create', options)
- .then(created => {
- pageId.value = created.id;
- currentName.value = name.value.trim();
- os.alert({
- type: 'success',
- text: i18n.ts._pages.created,
- });
- mainRouter.push(`/pages/edit/${pageId.value}`);
- }).catch(onError);
+ const created = await os.apiWithDialog('pages/create', options, undefined, {
+ '4650348e-301c-499a-83c9-6aa988c66bc1': {
+ title: i18n.ts.somethingHappened,
+ text: i18n.ts._pages.nameAlreadyExists,
+ },
+ });
+
+ pageId.value = created.id;
+ currentName.value = name.value.trim();
+ mainRouter.replace(`/pages/edit/${pageId.value}`);
}
}
-function del() {
- os.confirm({
+async function del() {
+ if (!pageId.value) return;
+
+ const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: title.value.trim() }),
- }).then(({ canceled }) => {
- if (canceled) return;
- misskeyApi('pages/delete', {
- pageId: pageId.value,
- }).then(() => {
- os.alert({
- type: 'success',
- text: i18n.ts._pages.deleted,
- });
- mainRouter.push('/pages');
- });
});
+
+ if (canceled) return;
+
+ await os.apiWithDialog('pages/delete', {
+ pageId: pageId.value,
+ });
+
+ mainRouter.replace('/pages');
}
-function duplicate() {
+async function duplicate() {
title.value = title.value + ' - copy';
name.value = name.value + '-copy';
- misskeyApi('pages/create', getSaveOptions()).then(created => {
- pageId.value = created.id;
- currentName.value = name.value.trim();
- os.alert({
- type: 'success',
- text: i18n.ts._pages.created,
- });
- mainRouter.push(`/pages/edit/${pageId.value}`);
+
+ const created = await os.apiWithDialog('pages/create', getSaveOptions(), undefined, {
+ '4650348e-301c-499a-83c9-6aa988c66bc1': {
+ title: i18n.ts.somethingHappened,
+ text: i18n.ts._pages.nameAlreadyExists,
+ },
});
+
+ pageId.value = created.id;
+ currentName.value = name.value.trim();
+
+ mainRouter.push(`/pages/edit/${pageId.value}`);
}
async function add() {
@@ -216,7 +205,7 @@ async function add() {
content.value.push({ id, type });
}
-function setEyeCatchingImage(img) {
+function setEyeCatchingImage(img: Event) {
selectFile(img.currentTarget ?? img.target, null).then(file => {
eyeCatchingImageId.value = file.id;
});
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 544e112111..d27a4f121d 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -266,7 +266,7 @@ function showMenu(ev: MouseEvent) {
if ($i && $i.id === page.value.userId) {
menuItems.push({
icon: 'ti ti-pencil',
- text: i18n.ts._pages.editThisPage,
+ text: i18n.ts.edit,
action: () => router.push(`/pages/edit/${page.value.id}`),
});
@@ -285,10 +285,6 @@ function showMenu(ev: MouseEvent) {
}
} else if ($i && $i.id !== page.value.userId) {
menuItems.push({
- icon: 'ti ti-code',
- text: i18n.ts._pages.viewSource,
- action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
- }, {
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 88171f7d70..983cba1746 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div class="_gaps_s">
<div :class="$style.editor" class="_panel">
- <MkCodeEditor v-model="code" lang="aiscript"/>
+ <MkCodeEditor v-model="code" lang="aiscript" debounce/>
</div>
<MkButton primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
</div>
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index d64537d289..d5f96efb8e 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -13,22 +13,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>{{ i18n.ts.options }}</template>
<div class="_gaps_m">
- <MkRadios v-model="hostSelect">
- <template #label>{{ i18n.ts.host }}</template>
- <option value="all" default>{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.local }}</option>
- <option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option>
- </MkRadios>
- <MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search">
- <template #prefix><i class="ti ti-server"></i></template>
- </MkInput>
- <MkSwitch v-model="order">Sort by newest to oldest</MkSwitch>
+ <template v-if="instance.federation !== 'none'">
+ <MkRadios v-model="hostSelect">
+ <template #label>{{ i18n.ts.host }}</template>
+ <option value="all" default>{{ i18n.ts.all }}</option>
+ <option value="local">{{ i18n.ts.local }}</option>
+ <option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option>
+ </MkRadios>
+ <MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search">
+ <template #prefix><i class="ti ti-server"></i></template>
+ </MkInput>
+ </template>
+
+ <MkSwitch v-model="order">{{ i18n.ts._noteSearch.newestToOldest }}</MkSwitch>
+
<MkSelect v-model="filetype" small>
- <template #label>File Type</template>
- <option :value="null">None</option>
- <option value="image">Images</option>
- <option value="video">Videos</option>
- <option value="audio">Audio</option>
+ <template #label>{{ i18n.ts._noteSearch.fileType }}</template>
+ <option :value="null">{{ i18n.ts._noteSearch._fileType.none }}</option>
+ <option value="image">{{ i18n.ts._noteSearch._fileType.image }}</option>
+ <option value="video">{{ i18n.ts._noteSearch._fileType.video }}</option>
+ <option value="audio">{{ i18n.ts._noteSearch._fileType.audio }}</option>
+ <option value="module">{{ i18n.ts._noteSearch._fileType.module }}</option>
+ <option value="flash">{{ i18n.ts._noteSearch._fileType.flash }}</option>
</MkSelect>
<MkFolder :defaultOpen="true">
@@ -97,7 +103,7 @@ const notePagination = ref<Paging>();
const user = ref<UserDetailed | null>(null);
const hostInput = ref(toRef(props, 'host').value);
const order = ref(false);
-const filetype = ref(null);
+const filetype = ref<'image' | 'video' | 'audio' | 'module' | 'flash' | null>(null);
const noteSearchableScope = instance.noteSearchableScope ?? 'local';
@@ -114,7 +120,7 @@ setHostSelectWithInput(hostInput.value, undefined);
watch(hostInput, setHostSelectWithInput);
const searchHost = computed(() => {
- if (hostSelect.value === 'local') return '.';
+ if (hostSelect.value === 'local' || instance.federation === 'none') return '.';
if (hostSelect.value === 'specified') return hostInput.value;
return null;
});
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index a355c0eeaa..8d0899a30c 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
- <MkRadios v-model="searchOrigin" @update:modelValue="search()">
+ <MkRadios v-if="instance.federation !== 'none'" v-model="searchOrigin" @update:modelValue="search()">
<option value="combined">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
@@ -33,6 +33,7 @@ import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
import * as os from '@/os.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -84,33 +85,26 @@ async function search() {
}
//#endregion
- if (query.length > 1 && !query.includes(' ')) {
- if (query.startsWith('@')) {
- const confirm = await os.confirm({
- type: 'info',
- text: i18n.ts.lookupConfirm,
- });
- if (!confirm.canceled) {
- router.push(`/${query}`);
- return;
- }
- }
-
- if (query.startsWith('#')) {
- const confirm = await os.confirm({
- type: 'info',
- text: i18n.ts.openTagPageConfirm,
- });
- if (!confirm.canceled) {
- router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`);
- return;
- }
+ if (query.length > 1 && !query.includes(' ') && query.startsWith('#')) {
+ const confirm = await os.confirm({
+ type: 'info',
+ text: i18n.ts.openTagPageConfirm,
+ });
+ if (!confirm.canceled) {
+ router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`);
+ return;
}
}
if (query.match(/^@[a-z0-9_.-]+@[a-z0-9_.-]+$/i)) {
- router.push(`/${query}`);
- return;
+ const confirm = await os.confirm({
+ type: 'info',
+ text: i18n.ts.lookupConfirm,
+ });
+ if (!confirm.canceled) {
+ router.push(`/${query}`);
+ return;
+ }
}
userPagination.value = {
@@ -118,7 +112,7 @@ async function search() {
limit: 10,
params: {
query: query,
- origin: searchOrigin.value,
+ origin: instance.federation === 'none' ? 'local' : searchOrigin.value,
},
};
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 97e960675f..c2588736b3 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -12,7 +12,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton>
</div>
- <MkUserCardMini v-for="user in accounts" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/>
+ <template v-for="[id, user] in accounts">
+ <MkUserCardMini v-if="user != null" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/>
+ <button v-else v-panel class="_button" :class="$style.unknownUser" @click="menu(id, $event)">
+ <div :class="$style.unknownUserAvatarMock"><i class="ti ti-user-question"></i></div>
+ <div>
+ <div :class="$style.unknownUserTitle">{{ i18n.ts.unknown }}</div>
+ <div :class="$style.unknownUserSub">ID: <span class="_monospace">{{ id }}</span></div>
+ </div>
+ </button>
+ </template>
</div>
</FormSuspense>
</div>
@@ -29,9 +38,10 @@ import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWith
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
+import { MenuItem } from '@/types/menu';
const storedAccounts = ref<{ id: string, token: string }[] | null>(null);
-const accounts = ref<Misskey.entities.UserDetailed[]>([]);
+const accounts = ref(new Map<string, Misskey.entities.UserDetailed | null>());
const init = async () => {
getAccounts().then(accounts => {
@@ -41,21 +51,35 @@ const init = async () => {
userIds: storedAccounts.value.map(x => x.id),
});
}).then(response => {
- accounts.value = response;
+ if (storedAccounts.value == null) return;
+ accounts.value = new Map(storedAccounts.value.map(x => [x.id, response.find((y: Misskey.entities.UserDetailed) => y.id === x.id) ?? null]));
});
};
-function menu(account: Misskey.entities.UserDetailed, ev: MouseEvent) {
- os.popupMenu([{
- text: i18n.ts.switch,
- icon: 'ti ti-switch-horizontal',
- action: () => switchAccount(account),
- }, {
- text: i18n.ts.logout,
- icon: 'ti ti-trash',
- danger: true,
- action: () => removeAccount(account),
- }], ev.currentTarget ?? ev.target);
+function menu(account: Misskey.entities.UserDetailed | string, ev: MouseEvent) {
+ let menu: MenuItem[];
+
+ if (typeof account === 'string') {
+ menu = [{
+ text: i18n.ts.logout,
+ icon: 'ti ti-trash',
+ danger: true,
+ action: () => removeAccount(account),
+ }];
+ } else {
+ menu = [{
+ text: i18n.ts.switch,
+ icon: 'ti ti-switch-horizontal',
+ action: () => switchAccount(account.id),
+ }, {
+ text: i18n.ts.logout,
+ icon: 'ti ti-trash',
+ danger: true,
+ action: () => removeAccount(account.id),
+ }];
+ }
+
+ os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function addAccount(ev: MouseEvent) {
@@ -68,9 +92,9 @@ function addAccount(ev: MouseEvent) {
}], ev.currentTarget ?? ev.target);
}
-async function removeAccount(account: Misskey.entities.UserDetailed) {
- await _removeAccount(account.id);
- accounts.value = accounts.value.filter(x => x.id !== account.id);
+async function removeAccount(id: string) {
+ await _removeAccount(id);
+ accounts.value.delete(id);
}
function addExistingAccount() {
@@ -90,9 +114,9 @@ function createAccount() {
});
}
-async function switchAccount(account: Misskey.entities.UserDetailed) {
+async function switchAccount(id: string) {
const fetchedAccounts = await getAccounts();
- const token = fetchedAccounts.find(x => x.id === account.id)!.token;
+ const token = fetchedAccounts.find(x => x.id === id)!.token;
switchAccountWithToken(token);
}
@@ -112,6 +136,49 @@ definePageMetadata(() => ({
<style lang="scss" module>
.user {
- cursor: pointer;
+ cursor: pointer;
+}
+
+.unknownUser {
+ display: flex;
+ align-items: center;
+ text-align: start;
+ padding: 16px;
+ background: var(--MI_THEME-panel);
+ border-radius: 8px;
+ font-size: 0.9em;
+}
+
+.unknownUserAvatarMock {
+ display: block;
+ width: 34px;
+ height: 34px;
+ line-height: 34px;
+ text-align: center;
+ font-size: 16px;
+ margin-right: 12px;
+ background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%);
+ color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%);
+ border-radius: 50%;
+}
+
+.unknownUserTitle {
+ display: block;
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 18px;
+}
+
+.unknownUserSub {
+ display: block;
+ width: 100%;
+ font-size: 95%;
+ opacity: 0.7;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 16px;
}
</style>
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index d452f249b6..d90c86a4ec 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -19,28 +19,6 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.receiveAnnouncementFromInstance }}
</MkSwitch>
</FormSection>
-
- <FormSection>
- <template #label>{{ i18n.ts.emailNotification }}</template>
-
- <div class="_gaps_s">
- <MkSwitch v-model="emailNotification_mention">
- {{ i18n.ts._notification._types.mention }}
- </MkSwitch>
- <MkSwitch v-model="emailNotification_reply">
- {{ i18n.ts._notification._types.reply }}
- </MkSwitch>
- <MkSwitch v-model="emailNotification_quote">
- {{ i18n.ts._notification._types.quote }}
- </MkSwitch>
- <MkSwitch v-model="emailNotification_follow">
- {{ i18n.ts._notification._types.follow }}
- </MkSwitch>
- <MkSwitch v-model="emailNotification_receiveFollowRequest">
- {{ i18n.ts._notification._types.receiveFollowRequest }}
- </MkSwitch>
- </div>
- </FormSection>
</div>
<div v-if="!instance.enableEmail" class="_gaps_m">
<MkInfo>{{ i18n.ts.emailNotSupported }}</MkInfo>
@@ -81,28 +59,6 @@ async function saveEmailAddress() {
});
}
-const emailNotification_mention = ref($i.emailNotificationTypes.includes('mention'));
-const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply'));
-const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote'));
-const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow'));
-const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest'));
-
-const saveNotificationSettings = () => {
- misskeyApi('i/update', {
- emailNotificationTypes: [
- ...[emailNotification_mention.value ? 'mention' : null],
- ...[emailNotification_reply.value ? 'reply' : null],
- ...[emailNotification_quote.value ? 'quote' : null],
- ...[emailNotification_follow.value ? 'follow' : null],
- ...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
- ].filter(x => x != null),
- });
-};
-
-watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest], () => {
- saveNotificationSettings();
-});
-
onMounted(() => {
watch(emailAddress, () => {
saveEmailAddress();
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index b42c2f1503..fc9c6aa669 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -14,14 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
</template>
</I18n>
- <!--
- <br />
- <I18n :src="i18n.ts.i18nInfoSharkey" tag="span">
- <template #link>
- <MkLink url="https://crowdin.com/project/misskey">INSERT THINGY</MkLink>
- </template>
- </I18n>
- -->
</template>
</MkSelect>
@@ -86,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
<div v-if="useCustomSearchEngine">
- <MkInput v-model="searchEngine" :max="300">
+ <MkInput v-model="searchEngine" :max="300" :manualSave="true">
<template #label>{{ i18n.ts.searchEngineCusomURI }}</template>
<template #caption>{{ i18n.ts.searchEngineCustomURIDescription }}</template>
</MkInput>
@@ -106,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
</div>
- <MkSelect v-model="instanceTicker">
+ <MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
<template #label>{{ i18n.ts.instanceTicker }}</template>
<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
@@ -152,11 +144,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkSwitch>
- <!-- {{ i18n.ts.notificationDotNotWorkingAdvice }} -->
-
- <!-- notificationDotNotWorkingAdvice -->
<MkButton @click="testNotificationDot">{{ i18n.ts.verifyNotificationDotWorkingButton }}</MkButton>
- <!-- <p class="caption">Testing Testing</p> -->
<MkRadios v-model="notificationPosition">
<template #label>{{ i18n.ts.position }}</template>
<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
@@ -340,7 +328,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, reactive, ref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { langs } from '@@/js/config.js';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -357,6 +345,7 @@ import MkInfo from '@/components/MkInfo.vue';
import { searchEngineMap } from '@/scripts/search-engine-map.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
+import { instance } from '@/instance.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import { i18n } from '@/i18n.js';
@@ -364,7 +353,6 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
import { globalEvents } from '@/events.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import { deepMerge } from '@/scripts/merge.js';
import { worksOnInstance } from '@/scripts/favicon-dot.js';
const lang = ref(miLocalStorage.getItem('lang'));
@@ -422,7 +410,6 @@ const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
const showTickerOnReplies = computed(defaultStore.makeGetterSetter('showTickerOnReplies'));
-//const searchEngine = computed(defaultStore.makeGetterSetter('searchEngine'));
const searchEngine = computed(defaultStore.makeGetterSetter('searchEngine'));
const noteDesign = computed(defaultStore.makeGetterSetter('noteDesign'));
@@ -470,7 +457,7 @@ watch(useSystemFont, () => {
watch(noteDesign, async (newval) => {
if (noteDesign.value === newval) {
- await reloadAsk();
+ await reloadAsk({});
}
});
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 552b4ee028..b7bf8c5dc1 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -43,7 +43,7 @@ const indexInfo = {
icon: 'ti ti-settings',
hideHeader: true,
};
-const INFO = ref(indexInfo);
+const INFO = ref<PageMetadata>(indexInfo);
const el = shallowRef<HTMLElement | null>(null);
const childInfo = ref<null | PageMetadata>(null);
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 82aeb6063f..d6ee45e074 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -9,17 +9,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ph-envelope ph-bold ph-lg"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template>
- <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
+ <div class="_gaps_m">
+ <MkInfo>{{ i18n.ts.wordMuteDescription }}</MkInfo>
+ <MkSwitch v-model="showSoftWordMutedWord">{{ i18n.ts.showMutedWord }}</MkSwitch>
+ <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
+ </div>
</MkFolder>
<MkFolder>
<template #icon><i class="ph-x-square ph-bold ph-lg"></i></template>
<template #label>{{ i18n.ts.hardWordMute }}</template>
- <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/>
+ <div class="_gaps_m">
+ <MkInfo>{{ i18n.ts.hardWordMuteDescription }}</MkInfo>
+ <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/>
+ </div>
</MkFolder>
- <MkFolder>
+ <MkFolder v-if="instance.federation !== 'none'">
<template #icon><i class="ti ti-planet-off"></i></template>
<template #label>{{ i18n.ts.instanceMute }}</template>
@@ -126,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, watch } from 'vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -135,9 +142,13 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js';
-import { infoImageUrl } from '@/instance.js';
+import { instance, infoImageUrl } from '@/instance.js';
import { signinRequired } from '@/account.js';
+import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { defaultStore } from '@/store';
+import { reloadAsk } from '@/scripts/reload-ask.js';
const $i = signinRequired();
@@ -160,6 +171,14 @@ const expandedRenoteMuteItems = ref([]);
const expandedMuteItems = ref([]);
const expandedBlockItems = ref([]);
+const showSoftWordMutedWord = computed(defaultStore.makeGetterSetter('showSoftWordMutedWord'));
+
+watch([
+ showSoftWordMutedWord,
+], async () => {
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
async function unrenoteMute(user, ev) {
os.popupMenu([{
text: i18n.ts.renoteUnmute,
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 790f9e44e2..0b8e89a6a5 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>
<div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
- <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
+ <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
</template>
</MkSwitch>
@@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>
<div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div>
- <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+ <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
</template>
</FormSlot>
@@ -129,7 +129,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>
<div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div>
- <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+ <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
</template>
</FormSlot>
</div>
@@ -155,10 +155,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
</div>
</MkFolder>
+
+ <MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
+
+ <MkInput v-model="defaultCW" type="text" manualSave @update:modelValue="save()">
+ <template #label>{{ i18n.ts.defaultCW }}</template>
+ <template #caption>{{ i18n.ts.defaultCWDescription }}</template>
+ </MkInput>
+
+ <MkSelect v-model="defaultCWPriority" :disabled="!defaultCW || !keepCw" @update:modelValue="save()">
+ <template #label>{{ i18n.ts.defaultCWPriority }}</template>
+ <template #caption>{{ i18n.ts.defaultCWPriorityDescription }}</template>
+ <option value="default">{{ i18n.ts._defaultCWPriority.default }}</option>
+ <option value="parent">{{ i18n.ts._defaultCWPriority.parent }}</option>
+ <option value="parentDefault">{{ i18n.ts._defaultCWPriority.parentDefault }}</option>
+ <option value="defaultParent">{{ i18n.ts._defaultCWPriority.defaultParent }}</option>
+ </MkSelect>
</div>
</FormSection>
-
- <MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
</div>
</template>
@@ -171,6 +185,7 @@ import MkFolder from '@/components/MkFolder.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
import { signinRequired } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import FormSlot from '@/components/form/slot.vue';
@@ -193,6 +208,8 @@ const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions);
const followingVisibility = ref($i.followingVisibility);
const followersVisibility = ref($i.followersVisibility);
+const defaultCW = ref($i.defaultCW);
+const defaultCWPriority = ref($i.defaultCWPriority);
const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
@@ -224,7 +241,7 @@ watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => {
});
async function update_requireSigninToViewContents(value: boolean) {
- if (value) {
+ if (value === true && instance.federation !== 'none') {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.acknowledgeNotesAndEnable,
@@ -251,6 +268,8 @@ function save() {
publicReactions: !!publicReactions.value,
followingVisibility: followingVisibility.value,
followersVisibility: followersVisibility.value,
+ defaultCWPriority: defaultCWPriority.value,
+ defaultCW: defaultCW.value,
});
}
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 9ff4a63e09..337cbfc532 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<MkInput v-model="profile.listenbrainz" manualSave>
- <template #label>ListenBrainz</template>
+ <template #label>{{ i18n.ts._profile.listenbrainz }}</template>
<template #prefix><i class="ph-headphones ph-bold ph-lg"></i></template>
</MkInput>
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index 67943524ef..140b6beb14 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="statusbar.type" placeholder="Please select">
<template #label>{{ i18n.ts.type }}</template>
<option value="rss">RSS</option>
- <option value="federation">Federation</option>
+ <option v-if="instance.federation !== 'none'" value="federation">Federation</option>
<option value="userList">User list timeline</option>
</MkSelect>
@@ -96,6 +96,7 @@ import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
import { deepClone } from '@/scripts/clone.js';
const props = defineProps<{
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index a530f4b5d6..e49d6af470 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
-import { toUnicode } from 'punycode/';
+import { toUnicode } from 'punycode.js';
import tinycolor from 'tinycolor2';
import { v4 as uuid } from 'uuid';
import JSON5 from 'json5';
diff --git a/packages/frontend/src/pages/user/files.vue b/packages/frontend/src/pages/user/files.vue
new file mode 100644
index 0000000000..b6c7c1c777
--- /dev/null
+++ b/packages/frontend/src/pages/user/files.vue
@@ -0,0 +1,56 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+ <MkSpacer :contentMax="1100">
+ <div :class="$style.root">
+ <MkPagination v-slot="{items}" :pagination="pagination">
+ <div :class="$style.stream">
+ <MkNoteMediaGrid v-for="note in items" :note="note" square/>
+ </div>
+ </MkPagination>
+ </div>
+ </MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue';
+import MkPagination from '@/components/MkPagination.vue';
+
+const props = defineProps<{
+ user: Misskey.entities.UserDetailed;
+}>();
+
+const pagination = {
+ endpoint: 'users/notes' as const,
+ limit: 15,
+ params: computed(() => ({
+ userId: props.user.id,
+ withFiles: true,
+ })),
+};
+</script>
+
+<style lang="scss" module>
+.root {
+ padding: 8px;
+}
+
+.stream {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: var(--MI-marginHalf);
+}
+
+@media screen and (min-width: 600px) {
+ .stream {
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ }
+
+}
+</style>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 5565555ca4..4b3773e0ae 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
<div :key="user.id" class="main _panel">
- <div class="banner-container" :style="style">
+ <div class="banner-container" :class="{ [$style.bannerContainerTall]: useTallBanner }" :style="style">
<div ref="bannerEl" class="banner" :style="style"></div>
<div class="fade"></div>
<div class="title">
@@ -39,12 +39,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<li v-if="user.isBlocking">{{ i18n.ts.blocked }}</li>
<li v-if="user.isBlocked && $i.isModerator">{{ i18n.ts.blockingYou }}</li>
</ul>
- <div class="actions">
- <button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
- <MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+ <div :class="$style.actions" class="actions">
+ <button :class="$style.actionsMenu" class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
+ <MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :class="$style.actionsFollow" :disabled="disableFollowControls" :inline="true" :transparent="false" :full="true" class="koudoku" @update:wait="onFollowButtonDisabledChanged"/>
+ <div v-if="hasFollowRequest" :class="$style.actionsBanner">{{ i18n.ts.receiveFollowRequest }}</div>
+ <MkButton v-if="hasFollowRequest" :class="$style.actionsAccept" :disabled="disableFollowControls" :inline="true" :transparent="false" :full="true" rounded primary @click="acceptFollowRequest"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
+ <MkButton v-if="hasFollowRequest" :class="$style.actionsReject" :disabled="disableFollowControls" :inline="true" :transparent="false" :full="true" rounded danger @click="rejectFollowRequest"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
</div>
</div>
- <MkAvatar class="avatar" :user="user" indicator/>
+ <MkAvatar class="avatar" :class="{ [$style.avatarTall]: useTallBanner }" :user="user" indicator/>
<div class="title">
<MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom">
@@ -138,7 +141,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="user.pinnedNotes.length === 0 && $i?.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
<template v-if="narrow">
<MkLazy>
- <XFiles :key="user.id" :user="user" :collapsed="true"/>
+ <XFiles :key="user.id" :user="user" :collapsed="true" @unfold="emit('unfoldFiles')"/>
</MkLazy>
<MkLazy>
<XActivity :key="user.id" :user="user" :collapsed="true"/>
@@ -180,7 +183,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
- <XFiles :key="user.id" :user="user"/>
+ <XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/>
<XActivity :key="user.id" :user="user"/>
<XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user"/>
</div>
@@ -220,8 +223,8 @@ import MkSparkle from '@/components/MkSparkle.vue';
const MkNote = defineAsyncComponent(() =>
defaultStore.state.noteDesign === 'sharkey'
- ? import('@/components/SkNote.vue')
- : import('@/components/MkNote.vue'),
+ ? import('@/components/SkNote.vue')
+ : import('@/components/MkNote.vue'),
);
function calcAge(birthdate: string): number {
@@ -242,7 +245,6 @@ function calcAge(birthdate: string): number {
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
const XListenBrainz = defineAsyncComponent(() => import('./index.listenbrainz.vue'));
-//const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed;
@@ -252,6 +254,10 @@ const props = withDefaults(defineProps<{
disableNotes: false,
});
+const emit = defineEmits<{
+ (ev: 'unfoldFiles'): void;
+}>();
+
const router = useRouter();
const user = ref(props.user);
@@ -307,7 +313,7 @@ const pagination = {
endpoint: 'users/featured-notes' as const,
limit: 10,
params: computed(() => ({
- userId: props.user.id
+ userId: props.user.id,
})),
};
@@ -384,6 +390,42 @@ async function updateMemo() {
isEditingMemo.value = false;
}
+// Set true to disable the follow / follow request controls
+const disableFollowControls = ref(false);
+const hasFollowRequest = computed(() => user.value.hasPendingFollowRequestToYou);
+const useTallBanner = computed(() => hasFollowRequest.value && narrow.value);
+
+async function onFollowButtonDisabledChanged(disabled: boolean) {
+ try {
+ // Refresh the UI after MkFollowButton changes the follow relation
+ if (!disabled) {
+ user.value = await os.apiWithDialog('users/show', { userId: user.value.id });
+ }
+ } finally {
+ disableFollowControls.value = disabled;
+ }
+}
+
+async function acceptFollowRequest() {
+ try {
+ disableFollowControls.value = true;
+ await os.apiWithDialog('following/requests/accept', { userId: user.value.id });
+ user.value = await os.apiWithDialog('users/show', { userId: user.value.id });
+ } finally {
+ disableFollowControls.value = false;
+ }
+}
+
+async function rejectFollowRequest() {
+ try {
+ disableFollowControls.value = true;
+ await os.apiWithDialog('following/requests/reject', { userId: user.value.id });
+ user.value = await os.apiWithDialog('users/show', { userId: user.value.id });
+ } finally {
+ disableFollowControls.value = false;
+ }
+}
+
watch([props.user], () => {
memoDraft.value = props.user.memo;
});
@@ -860,4 +902,48 @@ onUnmounted(() => {
margin-left: 8px;
}
}
+
+.actions {
+ display: grid;
+ grid-template-rows: min-content min-content min-content;
+ grid-template-columns: min-content auto 1fr;
+ grid-template-areas:
+ "menu follow follow"
+ "banner banner banner"
+ "accept accept reject";
+}
+
+.actionsMenu {
+ grid-area: menu;
+ width: unset;
+}
+
+.actionsFollow {
+ grid-area: follow;
+ margin-left: 8px;
+}
+
+.actionsBanner {
+ grid-area: banner;
+ justify-self: center;
+ margin-top: 8px;
+ margin-bottom: 4px;
+}
+
+.actionsAccept {
+ grid-area: accept;
+}
+
+.actionsReject {
+ grid-area: reject;
+ margin-left: 8px;
+}
+
+.bannerContainerTall {
+ height: 200px !important;
+}
+
+.avatarTall {
+ top: 150px !important;
+}
</style>
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index 7fe90da865..44e35e3479 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -4,30 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkContainer :max-height="300" :foldable="true" :expanded="!collapsed">
+<MkContainer :max-height="300" :foldable="true" :expanded="!collapsed" :onUnfold="unfoldContainer">
<template #icon><i class="ti ti-photo"></i></template>
<template #header>{{ i18n.ts.files }}</template>
<div :class="$style.root">
<MkLoading v-if="fetching"/>
- <div v-if="!fetching && files.length > 0" :class="$style.stream">
- <template v-for="file in files" :key="file.note.id + file.file.id">
- <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)">
- <!-- TODO: 画像以外のファイルに対応 -->
- <ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/>
- <div :class="$style.sensitive">
- <div>
- <div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
- <div>{{ i18n.ts.clickToShow }}</div>
- </div>
- </div>
- </div>
- <MkA v-else :class="$style.img" :to="notePage(file.note)">
- <!-- TODO: 画像以外のファイルに対応 -->
- <ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/>
- </MkA>
- </template>
+ <div v-if="!fetching && notes.length > 0" :class="$style.stream">
+ <MkNoteMediaGrid v-for="note in notes" :note="note"/>
</div>
- <p v-if="!fetching && files.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
+ <p v-if="!fetching && notes.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
</div>
</MkContainer>
</template>
@@ -35,13 +20,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { notePage } from '@/filters/note.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue';
-import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
-import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue';
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed;
@@ -50,33 +32,25 @@ const props = withDefaults(defineProps<{
collapsed: false,
});
+const emit = defineEmits<{
+ (ev: 'unfold'): void;
+}>();
+
const fetching = ref(true);
-const files = ref<{
- note: Misskey.entities.Note;
- file: Misskey.entities.DriveFile;
-}[]>([]);
-const showingFiles = ref<string[]>([]);
+const notes = ref<Misskey.entities.Note[]>([]);
-function thumbnail(image: Misskey.entities.DriveFile): string {
- return defaultStore.state.disableShowingAnimatedImages
- ? getStaticImageUrl(image.url)
- : image.thumbnailUrl;
+function unfoldContainer(): boolean {
+ emit('unfold');
+ return false;
}
onMounted(() => {
misskeyApi('users/notes', {
userId: props.user.id,
withFiles: true,
- limit: 15,
- }).then(notes => {
- for (const note of notes) {
- for (const file of note.files) {
- files.value.push({
- note,
- file,
- });
- }
- }
+ limit: 10,
+ }).then(_notes => {
+ notes.value = _notes;
fetching.value = false;
});
});
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index a35250bf5f..ba02559d68 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -9,10 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<div v-if="user">
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
- <XHome v-if="tab === 'home'" key="home" :user="user"/>
+ <XHome v-if="tab === 'home'" key="home" :user="user" @unfoldFiles="() => { tab = 'files'; }"/>
<MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800" style="padding-top: 0">
<XTimeline :user="user"/>
</MkSpacer>
+ <XFiles v-else-if="tab === 'files'" :user="user"/>
<XActivity v-else-if="tab === 'activity'" key="activity" :user="user"/>
<XAchievements v-else-if="tab === 'achievements'" key="achievements" :user="user"/>
<XReactions v-else-if="tab === 'reactions'" key="reactions" :user="user"/>
@@ -39,10 +40,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { getServerContext } from '@/server-context.js';
+import { serverContext, assertServerContext } from '@/server-context.js';
const XHome = defineAsyncComponent(() => import('./home.vue'));
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
+const XFiles = defineAsyncComponent(() => import('./files.vue'));
const XActivity = defineAsyncComponent(() => import('./activity.vue'));
const XAchievements = defineAsyncComponent(() => import('./achievements.vue'));
const XReactions = defineAsyncComponent(() => import('./reactions.vue'));
@@ -53,7 +55,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
-const CTX_USER = getServerContext('user');
+// contextは非ログイン状態の情報しかないためログイン時は利用できない
+const CTX_USER = !$i && assertServerContext(serverContext, 'user') ? serverContext.user : null;
const props = withDefaults(defineProps<{
acct: string;
@@ -103,6 +106,10 @@ const headerTabs = computed(() => user.value ? [{
title: i18n.ts.notes,
icon: 'ti ti-pencil',
}, {
+ key: 'files',
+ title: i18n.ts.files,
+ icon: 'ti ti-photo',
+}, {
key: 'activity',
title: i18n.ts.activity,
icon: 'ti ti-chart-line',
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index f1842255e0..c5731bd2a9 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -53,12 +53,14 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
if (!instance.iconUrl) {
return '';
}
+
return getProxiedImageUrl(instance.iconUrl, 'preview');
}
misskeyApiGet('federation/instances', {
sort: '+pubSub',
limit: 20,
+ blocked: 'false',
}).then(_instances => {
instances.value = _instances;
});
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index c0034d414c..27bb34da36 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -6,8 +6,6 @@
import { ref } from 'vue';
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
-import * as os from '@/os.js';
-import { i18n } from '@/i18n.js';
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index c7637a1db9..2d50a27dbf 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -17,10 +17,7 @@ export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
});
const routes: RouteDef[] = [{
- path: '/@:initUser/pages/:initPageName/view-source',
- component: page(() => import('@/pages/page-editor/page-editor.vue')),
-}, {
- path: '/@:username/pages/:pageName',
+ path: '/@:username/pages/:pageName(*)',
component: page(() => import('@/pages/page.vue')),
}, {
path: '/@:acct/following',
@@ -391,6 +388,10 @@ const routes: RouteDef[] = [{
name: 'emojis',
component: page(() => import('@/pages/custom-emojis-manager.vue')),
}, {
+ path: '/emojis2',
+ name: 'emojis2',
+ component: page(() => import('@/pages/admin/custom-emojis-manager2.vue')),
+ }, {
path: '/avatar-decorations',
name: 'avatarDecorations',
component: page(() => import('@/pages/avatar-decorations.vue')),
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts
index 46aed49330..e203c51bba 100644
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ b/packages/frontend/src/scripts/aiscript/api.ts
@@ -3,14 +3,24 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { utils, values } from '@syuilo/aiscript';
+import { errors, utils, values } from '@syuilo/aiscript';
import * as Misskey from 'misskey-js';
+import { url, lang } from '@@/js/config.js';
+import { assertStringAndIsIn } from './common.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
-import { url, lang } from '@@/js/config.js';
+
+const DIALOG_TYPES = [
+ 'error',
+ 'info',
+ 'success',
+ 'warning',
+ 'waiting',
+ 'question',
+] as const;
export function aiScriptReadline(q: string): Promise<string> {
return new Promise(ok => {
@@ -22,15 +32,20 @@ export function aiScriptReadline(q: string): Promise<string> {
});
}
-export function createAiScriptEnv(opts) {
+export function createAiScriptEnv(opts: { storageKey: string, token?: string }) {
return {
USER_ID: $i ? values.STR($i.id) : values.NULL,
- USER_NAME: $i ? values.STR($i.name) : values.NULL,
+ USER_NAME: $i?.name ? values.STR($i.name) : values.NULL,
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
LOCALE: values.STR(lang),
SERVER_URL: values.STR(url),
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
+ utils.assertString(title);
+ utils.assertString(text);
+ if (type != null) {
+ assertStringAndIsIn(type, DIALOG_TYPES);
+ }
await os.alert({
type: type ? type.value : 'info',
title: title.value,
@@ -39,6 +54,11 @@ export function createAiScriptEnv(opts) {
return values.NULL;
}),
'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
+ utils.assertString(title);
+ utils.assertString(text);
+ if (type != null) {
+ assertStringAndIsIn(type, DIALOG_TYPES);
+ }
const confirm = await os.confirm({
type: type ? type.value : 'question',
title: title.value,
@@ -48,14 +68,20 @@ export function createAiScriptEnv(opts) {
}),
'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
utils.assertString(ep);
- if (ep.value.includes('://')) throw new Error('invalid endpoint');
+ if (ep.value.includes('://')) {
+ throw new errors.AiScriptRuntimeError('invalid endpoint');
+ }
if (token) {
utils.assertString(token);
// バグがあればundefinedもあり得るため念のため
if (typeof token.value !== 'string') throw new Error('invalid token');
}
const actualToken: string|null = token?.value ?? opts.token ?? null;
- return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => {
+ if (param == null) {
+ throw new errors.AiScriptRuntimeError('expected param');
+ }
+ utils.assertObject(param);
+ return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));
@@ -75,12 +101,18 @@ export function createAiScriptEnv(opts) {
*/
'Mk:save': values.FN_NATIVE(([key, value]) => {
utils.assertString(key);
+ utils.expectAny(value);
miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));
return values.NULL;
}),
'Mk:load': values.FN_NATIVE(([key]) => {
utils.assertString(key);
- return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`)));
+ return utils.jsToVal(miLocalStorage.getItemAsJson(`aiscript:${opts.storageKey}:${key.value}`) ?? null);
+ }),
+ 'Mk:remove': values.FN_NATIVE(([key]) => {
+ utils.assertString(key);
+ miLocalStorage.removeItem(`aiscript:${opts.storageKey}:${key.value}`);
+ return values.NULL;
}),
'Mk:url': values.FN_NATIVE(() => {
return values.STR(window.location.href);
diff --git a/packages/frontend/src/scripts/aiscript/common.ts b/packages/frontend/src/scripts/aiscript/common.ts
new file mode 100644
index 0000000000..de6fa1d633
--- /dev/null
+++ b/packages/frontend/src/scripts/aiscript/common.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { errors, utils, type values } from '@syuilo/aiscript';
+
+export function assertStringAndIsIn<A extends readonly string[]>(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } {
+ utils.assertString(value);
+ const str = value.value;
+ if (!expects.includes(str)) {
+ const expected = expects.map((expect) => `"${expect}"`).join(', ');
+ throw new errors.AiScriptRuntimeError(`"${value.value}" is not in ${expected}`);
+ }
+}
diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts
index 2b386bebb8..ca92b27ff5 100644
--- a/packages/frontend/src/scripts/aiscript/ui.ts
+++ b/packages/frontend/src/scripts/aiscript/ui.ts
@@ -7,6 +7,15 @@ import { utils, values } from '@syuilo/aiscript';
import { v4 as uuid } from 'uuid';
import { ref, Ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { assertStringAndIsIn } from './common.js';
+
+const ALIGNS = ['left', 'center', 'right'] as const;
+const FONTS = ['serif', 'sans-serif', 'monospace'] as const;
+const BORDER_STYLES = ['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] as const;
+
+type Align = (typeof ALIGNS)[number];
+type Font = (typeof FONTS)[number];
+type BorderStyle = (typeof BORDER_STYLES)[number];
export type AsUiComponentBase = {
id: string;
@@ -21,13 +30,13 @@ export type AsUiRoot = AsUiComponentBase & {
export type AsUiContainer = AsUiComponentBase & {
type: 'container';
children?: AsUiComponent['id'][];
- align?: 'left' | 'center' | 'right';
+ align?: Align;
bgColor?: string;
fgColor?: string;
- font?: 'serif' | 'sans-serif' | 'monospace';
+ font?: Font;
borderWidth?: number;
borderColor?: string;
- borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset';
+ borderStyle?: BorderStyle;
borderRadius?: number;
padding?: number;
rounded?: boolean;
@@ -40,7 +49,7 @@ export type AsUiText = AsUiComponentBase & {
size?: number;
bold?: boolean;
color?: string;
- font?: 'serif' | 'sans-serif' | 'monospace';
+ font?: Font;
};
export type AsUiMfm = AsUiComponentBase & {
@@ -49,14 +58,14 @@ export type AsUiMfm = AsUiComponentBase & {
size?: number;
bold?: boolean;
color?: string;
- font?: 'serif' | 'sans-serif' | 'monospace';
- onClickEv?: (evId: string) => void
+ font?: Font;
+ onClickEv?: (evId: string) => Promise<void>;
};
export type AsUiButton = AsUiComponentBase & {
type: 'button';
text?: string;
- onClick?: () => void;
+ onClick?: () => Promise<void>;
primary?: boolean;
rounded?: boolean;
disabled?: boolean;
@@ -69,7 +78,7 @@ export type AsUiButtons = AsUiComponentBase & {
export type AsUiSwitch = AsUiComponentBase & {
type: 'switch';
- onChange?: (v: boolean) => void;
+ onChange?: (v: boolean) => Promise<void>;
default?: boolean;
label?: string;
caption?: string;
@@ -77,7 +86,7 @@ export type AsUiSwitch = AsUiComponentBase & {
export type AsUiTextarea = AsUiComponentBase & {
type: 'textarea';
- onInput?: (v: string) => void;
+ onInput?: (v: string) => Promise<void>;
default?: string;
label?: string;
caption?: string;
@@ -85,7 +94,7 @@ export type AsUiTextarea = AsUiComponentBase & {
export type AsUiTextInput = AsUiComponentBase & {
type: 'textInput';
- onInput?: (v: string) => void;
+ onInput?: (v: string) => Promise<void>;
default?: string;
label?: string;
caption?: string;
@@ -93,7 +102,7 @@ export type AsUiTextInput = AsUiComponentBase & {
export type AsUiNumberInput = AsUiComponentBase & {
type: 'numberInput';
- onInput?: (v: number) => void;
+ onInput?: (v: number) => Promise<void>;
default?: number;
label?: string;
caption?: string;
@@ -105,7 +114,7 @@ export type AsUiSelect = AsUiComponentBase & {
text: string;
value: string;
}[];
- onChange?: (v: string) => void;
+ onChange?: (v: string) => Promise<void>;
default?: string;
label?: string;
caption?: string;
@@ -140,11 +149,15 @@ export type AsUiPostForm = AsUiComponentBase & {
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm;
+type Options<T extends AsUiComponent> = T extends AsUiButtons
+ ? Omit<T, 'id' | 'type' | 'buttons'> & { 'buttons'?: Options<AsUiButton>[] }
+ : Omit<T, 'id' | 'type'>;
+
export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
// TODO
}
-function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 'type'> {
+function getRootOptions(def: values.Value | undefined): Options<AsUiRoot> {
utils.assertObject(def);
const children = def.value.get('children');
@@ -153,30 +166,32 @@ function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 't
return {
children: children.value.map(v => {
utils.assertObject(v);
- return v.value.get('id').value;
+ const id = v.value.get('id');
+ utils.assertString(id);
+ return id.value;
}),
};
}
-function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, 'id' | 'type'> {
+function getContainerOptions(def: values.Value | undefined): Options<AsUiContainer> {
utils.assertObject(def);
const children = def.value.get('children');
if (children) utils.assertArray(children);
const align = def.value.get('align');
- if (align) utils.assertString(align);
+ if (align) assertStringAndIsIn(align, ALIGNS);
const bgColor = def.value.get('bgColor');
if (bgColor) utils.assertString(bgColor);
const fgColor = def.value.get('fgColor');
if (fgColor) utils.assertString(fgColor);
const font = def.value.get('font');
- if (font) utils.assertString(font);
+ if (font) assertStringAndIsIn(font, FONTS);
const borderWidth = def.value.get('borderWidth');
if (borderWidth) utils.assertNumber(borderWidth);
const borderColor = def.value.get('borderColor');
if (borderColor) utils.assertString(borderColor);
const borderStyle = def.value.get('borderStyle');
- if (borderStyle) utils.assertString(borderStyle);
+ if (borderStyle) assertStringAndIsIn(borderStyle, BORDER_STYLES);
const borderRadius = def.value.get('borderRadius');
if (borderRadius) utils.assertNumber(borderRadius);
const padding = def.value.get('padding');
@@ -189,7 +204,9 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
return {
children: children ? children.value.map(v => {
utils.assertObject(v);
- return v.value.get('id').value;
+ const id = v.value.get('id');
+ utils.assertString(id);
+ return id.value;
}) : [],
align: align?.value,
fgColor: fgColor?.value,
@@ -205,7 +222,7 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
};
}
-function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 'type'> {
+function getTextOptions(def: values.Value | undefined): Options<AsUiText> {
utils.assertObject(def);
const text = def.value.get('text');
@@ -217,7 +234,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
const color = def.value.get('color');
if (color) utils.assertString(color);
const font = def.value.get('font');
- if (font) utils.assertString(font);
+ if (font) assertStringAndIsIn(font, FONTS);
return {
text: text?.value,
@@ -228,7 +245,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
};
}
-function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> {
+function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiMfm> {
utils.assertObject(def);
const text = def.value.get('text');
@@ -240,7 +257,7 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg
const color = def.value.get('color');
if (color) utils.assertString(color);
const font = def.value.get('font');
- if (font) utils.assertString(font);
+ if (font) assertStringAndIsIn(font, FONTS);
const onClickEv = def.value.get('onClickEv');
if (onClickEv) utils.assertFunction(onClickEv);
@@ -250,13 +267,13 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg
bold: bold?.value,
color: color?.value,
font: font?.value,
- onClickEv: (evId: string) => {
- if (onClickEv) call(onClickEv, [values.STR(evId)]);
+ onClickEv: async (evId: string) => {
+ if (onClickEv) await call(onClickEv, [values.STR(evId)]);
},
};
}
-function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextInput, 'id' | 'type'> {
+function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextInput> {
utils.assertObject(def);
const onInput = def.value.get('onInput');
@@ -269,8 +286,8 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF
if (caption) utils.assertString(caption);
return {
- onInput: (v) => {
- if (onInput) call(onInput, [utils.jsToVal(v)]);
+ onInput: async (v) => {
+ if (onInput) await call(onInput, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@@ -278,7 +295,7 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF
};
}
-function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextarea, 'id' | 'type'> {
+function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextarea> {
utils.assertObject(def);
const onInput = def.value.get('onInput');
@@ -291,8 +308,8 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn
if (caption) utils.assertString(caption);
return {
- onInput: (v) => {
- if (onInput) call(onInput, [utils.jsToVal(v)]);
+ onInput: async (v) => {
+ if (onInput) await call(onInput, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@@ -300,7 +317,7 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn
};
}
-function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiNumberInput, 'id' | 'type'> {
+function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiNumberInput> {
utils.assertObject(def);
const onInput = def.value.get('onInput');
@@ -313,8 +330,8 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.
if (caption) utils.assertString(caption);
return {
- onInput: (v) => {
- if (onInput) call(onInput, [utils.jsToVal(v)]);
+ onInput: async (v) => {
+ if (onInput) await call(onInput, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@@ -322,7 +339,7 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.
};
}
-function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButton, 'id' | 'type'> {
+function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButton> {
utils.assertObject(def);
const text = def.value.get('text');
@@ -338,8 +355,8 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn,
return {
text: text?.value,
- onClick: () => {
- if (onClick) call(onClick, []);
+ onClick: async () => {
+ if (onClick) await call(onClick, []);
},
primary: primary?.value,
rounded: rounded?.value,
@@ -347,7 +364,7 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn,
};
}
-function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButtons, 'id' | 'type'> {
+function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButtons> {
utils.assertObject(def);
const buttons = def.value.get('buttons');
@@ -369,8 +386,8 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn,
return {
text: text.value,
- onClick: () => {
- call(onClick, []);
+ onClick: async () => {
+ await call(onClick, []);
},
primary: primary?.value,
rounded: rounded?.value,
@@ -380,7 +397,7 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn,
};
}
-function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSwitch, 'id' | 'type'> {
+function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSwitch> {
utils.assertObject(def);
const onChange = def.value.get('onChange');
@@ -393,8 +410,8 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn,
if (caption) utils.assertString(caption);
return {
- onChange: (v) => {
- if (onChange) call(onChange, [utils.jsToVal(v)]);
+ onChange: async (v) => {
+ if (onChange) await call(onChange, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@@ -402,7 +419,7 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn,
};
}
-function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSelect, 'id' | 'type'> {
+function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSelect> {
utils.assertObject(def);
const items = def.value.get('items');
@@ -428,8 +445,8 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn,
value: value ? value.value : text.value,
};
}) : [],
- onChange: (v) => {
- if (onChange) call(onChange, [utils.jsToVal(v)]);
+ onChange: async (v) => {
+ if (onChange) await call(onChange, [utils.jsToVal(v)]);
},
default: defaultValue?.value,
label: label?.value,
@@ -437,7 +454,7 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn,
};
}
-function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id' | 'type'> {
+function getFolderOptions(def: values.Value | undefined): Options<AsUiFolder> {
utils.assertObject(def);
const children = def.value.get('children');
@@ -450,7 +467,9 @@ function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id'
return {
children: children ? children.value.map(v => {
utils.assertObject(v);
- return v.value.get('id').value;
+ const id = v.value.get('id');
+ utils.assertString(id);
+ return id.value;
}) : [],
title: title?.value ?? '',
opened: opened?.value ?? true,
@@ -475,7 +494,7 @@ function getPostFormProps(form: values.VObj): PostFormPropsForAsUi {
};
}
-function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostFormButton, 'id' | 'type'> {
+function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostFormButton> {
utils.assertObject(def);
const text = def.value.get('text');
@@ -497,7 +516,7 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu
};
}
-function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostForm, 'id' | 'type'> {
+function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostForm> {
utils.assertObject(def);
const form = def.value.get('form');
@@ -511,18 +530,26 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn
}
export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
+ type OptionsConverter<T extends AsUiComponent, C> = (def: values.Value | undefined, call: C) => Options<T>;
+
const instances = {};
- function createComponentInstance(type: AsUiComponent['type'], def: values.Value | undefined, id: values.Value | undefined, getOptions: (def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) => any, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
+ function createComponentInstance<T extends AsUiComponent, C>(
+ type: T['type'],
+ def: values.Value | undefined,
+ id: values.Value | undefined,
+ getOptions: OptionsConverter<T, C>,
+ call: C,
+ ) {
if (id) utils.assertString(id);
const _id = id?.value ?? uuid();
const component = ref({
...getOptions(def, call),
type,
id: _id,
- });
+ } as T);
components.push(component);
- const instance = values.OBJ(new Map([
+ const instance = values.OBJ(new Map<string, values.Value>([
['id', values.STR(_id)],
['update', values.FN_NATIVE(([def], opts) => {
utils.assertObject(def);
@@ -547,7 +574,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
'Ui:patch': values.FN_NATIVE(([id, val], opts) => {
utils.assertString(id);
utils.assertArray(val);
- patch(id.value, val.value, opts.call);
+ // patch(id.value, val.value, opts.call); // TODO
}),
'Ui:get': values.FN_NATIVE(([id], opts) => {
@@ -566,7 +593,9 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
rootComponent.value.children = children.value.map(v => {
utils.assertObject(v);
- return v.value.get('id').value;
+ const id = v.value.get('id');
+ utils.assertString(id);
+ return id.value;
});
}),
diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts
index d942402ffc..8a3a6bf6db 100644
--- a/packages/frontend/src/scripts/autocomplete.ts
+++ b/packages/frontend/src/scripts/autocomplete.ts
@@ -5,7 +5,7 @@
import { nextTick, Ref, ref, defineAsyncComponent } from 'vue';
import getCaretCoordinates from 'textarea-caret';
-import { toASCII } from 'punycode/';
+import { toASCII } from 'punycode.js';
import { popup } from '@/os.js';
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
diff --git a/packages/frontend/src/scripts/boost-quote.ts b/packages/frontend/src/scripts/boost-quote.ts
index 4e025f5d4f..feb949772b 100644
--- a/packages/frontend/src/scripts/boost-quote.ts
+++ b/packages/frontend/src/scripts/boost-quote.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { ref, Ref } from 'vue';
+import { ref, Ref, computed, ComputedRef } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -79,3 +79,11 @@ export function boostMenuItems(appearNote: Ref<Misskey.entities.Note>, renote: (
} as MenuItem,
];
}
+
+export function computeRenoteTooltip(renoted: Ref<boolean>): ComputedRef<string> {
+ return computed(() => {
+ if (renoted.value) return i18n.ts.unrenote;
+ if (defaultStore.state.showVisibilitySelectorOnBoost) return i18n.ts.renote;
+ return i18n.ts.renoteShift;
+ });
+}
diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts
index 6525c207f7..194ef0f420 100644
--- a/packages/frontend/src/scripts/check-word-mute.ts
+++ b/packages/frontend/src/scripts/check-word-mute.ts
@@ -4,7 +4,7 @@
*/
import * as Misskey from 'misskey-js';
-export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
+export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
// 自分自身
if (me && (note.userId === me.id)) return false;
@@ -13,7 +13,7 @@ export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.
if (text === '') return false;
- const matched = mutedWords.some(filter => {
+ const matched = mutedWords.filter(filter => {
if (Array.isArray(filter)) {
// Clean up
const filteredFilter = filter.filter(keyword => keyword !== '');
@@ -36,7 +36,7 @@ export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.
}
});
- if (matched) return true;
+ if (matched.length > 0) return matched;
}
return false;
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index 6710d9826e..4d57dcd944 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { createHighlighterCore, loadWasm } from 'shiki/core';
+import { createHighlighterCore } from 'shiki/core';
+import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
import darkPlus from 'shiki/themes/dark-plus.mjs';
import { bundledThemesInfo } from 'shiki/themes';
import { bundledLanguagesInfo } from 'shiki/langs';
@@ -60,8 +61,6 @@ export async function getHighlighter(): Promise<HighlighterCore> {
}
async function initHighlighter() {
- await loadWasm(import('shiki/onig.wasm?init'));
-
// テーマの重複を消す
const themes = unique([
darkPlus,
@@ -70,6 +69,7 @@ async function initHighlighter() {
const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript');
const highlighter = await createHighlighterCore({
+ engine: createOnigurumaEngine(() => import('shiki/onig.wasm?init')),
themes,
langs: [
...(jsLangInfo ? [async () => await jsLangInfo.import()] : []),
diff --git a/packages/frontend/src/scripts/file-drop.ts b/packages/frontend/src/scripts/file-drop.ts
new file mode 100644
index 0000000000..c2e863c0dc
--- /dev/null
+++ b/packages/frontend/src/scripts/file-drop.ts
@@ -0,0 +1,121 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type DroppedItem = DroppedFile | DroppedDirectory;
+
+export type DroppedFile = {
+ isFile: true;
+ path: string;
+ file: File;
+};
+
+export type DroppedDirectory = {
+ isFile: false;
+ path: string;
+ children: DroppedItem[];
+}
+
+export async function extractDroppedItems(ev: DragEvent): Promise<DroppedItem[]> {
+ const dropItems = ev.dataTransfer?.items;
+ if (!dropItems || dropItems.length === 0) {
+ return [];
+ }
+
+ const apiTestItem = dropItems[0];
+ if ('webkitGetAsEntry' in apiTestItem) {
+ return readDataTransferItems(dropItems);
+ } else {
+ // webkitGetAsEntryに対応していない場合はfilesから取得する(ディレクトリのサポートは出来ない)
+ const dropFiles = ev.dataTransfer.files;
+ if (dropFiles.length === 0) {
+ return [];
+ }
+
+ const droppedFiles = Array.of<DroppedFile>();
+ for (let i = 0; i < dropFiles.length; i++) {
+ const file = dropFiles.item(i);
+ if (file) {
+ droppedFiles.push({
+ isFile: true,
+ path: file.name,
+ file,
+ });
+ }
+ }
+
+ return droppedFiles;
+ }
+}
+
+/**
+ * ドラッグ&ドロップされたファイルのリストからディレクトリ構造とファイルへの参照({@link File})を取得する。
+ */
+export async function readDataTransferItems(itemList: DataTransferItemList): Promise<DroppedItem[]> {
+ async function readEntry(entry: FileSystemEntry): Promise<DroppedItem> {
+ if (entry.isFile) {
+ return {
+ isFile: true,
+ path: entry.fullPath,
+ file: await readFile(entry as FileSystemFileEntry),
+ };
+ } else {
+ return {
+ isFile: false,
+ path: entry.fullPath,
+ children: await readDirectory(entry as FileSystemDirectoryEntry),
+ };
+ }
+ }
+
+ function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise<File> {
+ return new Promise((resolve, reject) => {
+ fileSystemFileEntry.file(resolve, reject);
+ });
+ }
+
+ function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
+ return new Promise(async (resolve) => {
+ const allEntries = Array.of<FileSystemEntry>();
+ const reader = fileSystemDirectoryEntry.createReader();
+ while (true) {
+ const entries = await new Promise<FileSystemEntry[]>((res, rej) => reader.readEntries(res, rej));
+ if (entries.length === 0) {
+ break;
+ }
+ allEntries.push(...entries);
+ }
+
+ resolve(await Promise.all(allEntries.map(readEntry)));
+ });
+ }
+
+ // 扱いにくいので配列に変換
+ const items = Array.of<DataTransferItem>();
+ for (let i = 0; i < itemList.length; i++) {
+ items.push(itemList[i]);
+ }
+
+ return Promise.all(
+ items
+ .map(it => it.webkitGetAsEntry())
+ .filter(it => it)
+ .map(it => readEntry(it!)),
+ );
+}
+
+/**
+ * {@link DroppedItem}のリストからディレクトリを再帰的に検索し、ファイルのリストを取得する。
+ */
+export function flattenDroppedFiles(items: DroppedItem[]): DroppedFile[] {
+ const result = Array.of<DroppedFile>();
+ for (const item of items) {
+ if (item.isFile) {
+ result.push(item);
+ } else {
+ result.push(...flattenDroppedFiles(item.children));
+ }
+ }
+ return result;
+}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index c56fd185b6..9112daf49f 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -5,19 +5,19 @@
import { defineAsyncComponent, Ref, ShallowRef } from 'vue';
import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
import { claimAchievement } from './achievements.js';
+import type { MenuItem } from '@/types/menu.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { url } from '@@/js/config.js';
import { defaultStore, noteActions } from '@/store.js';
import { miLocalStorage } from '@/local-storage.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import { clipsCache, favoritedChannelsCache } from '@/cache.js';
-import type { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { isSupportShare } from '@/scripts/navigator.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
@@ -205,7 +205,7 @@ export function getNoteMenu(props: {
noteId: appearNote.id,
});
- if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) {
+ if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
claimAchievement('noteDeletedWithin1min');
}
});
@@ -224,7 +224,7 @@ export function getNoteMenu(props: {
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
- if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) {
+ if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
claimAchievement('noteDeletedWithin1min');
}
});
@@ -259,11 +259,6 @@ export function getNoteMenu(props: {
os.success();
}
- function copyLink(): void {
- copyToClipboard(`${url}/notes/${appearNote.id}`);
- os.success();
- }
-
function togglePin(pin: boolean): void {
os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
noteId: appearNote.id,
@@ -347,6 +342,13 @@ export function getNoteMenu(props: {
getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)')
);
menuItems.push({
+ icon: 'ti ti-link',
+ text: i18n.ts.copyRemoteLink,
+ action: () => {
+ copyToClipboard(appearNote.url ?? appearNote.uri);
+ os.success();
+ },
+ }, {
icon: 'ti ti-external-link',
text: i18n.ts.showOnRemote,
action: () => {
@@ -508,6 +510,13 @@ export function getNoteMenu(props: {
getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)')
);
menuItems.push({
+ icon: 'ti ti-link',
+ text: i18n.ts.copyRemoteLink,
+ action: () => {
+ copyToClipboard(appearNote.url ?? appearNote.uri);
+ os.success();
+ },
+ }, {
icon: 'ti ti-external-link',
text: i18n.ts.showOnRemote,
action: () => {
diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts
index 58d486bf9b..4e093bcf4c 100644
--- a/packages/frontend/src/scripts/get-note-summary.ts
+++ b/packages/frontend/src/scripts/get-note-summary.ts
@@ -4,6 +4,7 @@
*/
import * as Misskey from 'misskey-js';
+import { appendContentWarning } from '@@/js/append-content-warning.js';
import { i18n } from '@/i18n.js';
/**
@@ -25,9 +26,15 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => {
let summary = '';
+ // Append mandatory CW, if applicable
+ let cw = note.cw;
+ if (note.user.mandatoryCW) {
+ cw = appendContentWarning(cw, note.user.mandatoryCW);
+ }
+
// 本文
- if (note.cw != null) {
- summary += `CW: ${note.cw}`;
+ if (cw != null) {
+ summary += `CW: ${cw}`;
} else if (note.text) {
summary += note.text;
}
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 090cffe203..2fbdaf5d3c 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { toUnicode } from 'punycode';
+import { toUnicode } from 'punycode.js';
import { defineAsyncComponent, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/scripts/key-event.ts b/packages/frontend/src/scripts/key-event.ts
new file mode 100644
index 0000000000..a72776d48c
--- /dev/null
+++ b/packages/frontend/src/scripts/key-event.ts
@@ -0,0 +1,153 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * {@link KeyboardEvent.code} の値を表す文字列。不足分は適宜追加する
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
+ */
+export type KeyCode =
+ | 'Backspace'
+ | 'Tab'
+ | 'Enter'
+ | 'Shift'
+ | 'Control'
+ | 'Alt'
+ | 'Pause'
+ | 'CapsLock'
+ | 'Escape'
+ | 'Space'
+ | 'PageUp'
+ | 'PageDown'
+ | 'End'
+ | 'Home'
+ | 'ArrowLeft'
+ | 'ArrowUp'
+ | 'ArrowRight'
+ | 'ArrowDown'
+ | 'Insert'
+ | 'Delete'
+ | 'Digit0'
+ | 'Digit1'
+ | 'Digit2'
+ | 'Digit3'
+ | 'Digit4'
+ | 'Digit5'
+ | 'Digit6'
+ | 'Digit7'
+ | 'Digit8'
+ | 'Digit9'
+ | 'KeyA'
+ | 'KeyB'
+ | 'KeyC'
+ | 'KeyD'
+ | 'KeyE'
+ | 'KeyF'
+ | 'KeyG'
+ | 'KeyH'
+ | 'KeyI'
+ | 'KeyJ'
+ | 'KeyK'
+ | 'KeyL'
+ | 'KeyM'
+ | 'KeyN'
+ | 'KeyO'
+ | 'KeyP'
+ | 'KeyQ'
+ | 'KeyR'
+ | 'KeyS'
+ | 'KeyT'
+ | 'KeyU'
+ | 'KeyV'
+ | 'KeyW'
+ | 'KeyX'
+ | 'KeyY'
+ | 'KeyZ'
+ | 'MetaLeft'
+ | 'MetaRight'
+ | 'ContextMenu'
+ | 'F1'
+ | 'F2'
+ | 'F3'
+ | 'F4'
+ | 'F5'
+ | 'F6'
+ | 'F7'
+ | 'F8'
+ | 'F9'
+ | 'F10'
+ | 'F11'
+ | 'F12'
+ | 'NumLock'
+ | 'ScrollLock'
+ | 'Semicolon'
+ | 'Equal'
+ | 'Comma'
+ | 'Minus'
+ | 'Period'
+ | 'Slash'
+ | 'Backquote'
+ | 'BracketLeft'
+ | 'Backslash'
+ | 'BracketRight'
+ | 'Quote'
+ | 'Meta'
+ | 'AltGraph'
+ ;
+
+/**
+ * 修飾キーを表す文字列。不足分は適宜追加する。
+ */
+export type KeyModifier =
+ | 'Shift'
+ | 'Control'
+ | 'Alt'
+ | 'Meta'
+ ;
+
+/**
+ * 押下されたキー以外の状態を表す文字列。不足分は適宜追加する。
+ */
+export type KeyState =
+ | 'composing'
+ | 'repeat'
+ ;
+
+export type KeyEventHandler = {
+ modifiers?: KeyModifier[];
+ states?: KeyState[];
+ code: KeyCode | 'any';
+ handler: (event: KeyboardEvent) => void;
+}
+
+export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) {
+ function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) {
+ if (modifiers) {
+ return modifiers.every(modifier => ev.getModifierState(modifier));
+ }
+ return true;
+ }
+
+ function checkState(ev: KeyboardEvent, states?: KeyState[]) {
+ if (states) {
+ return states.every(state => ev.getModifierState(state));
+ }
+ return true;
+ }
+
+ let hit = false;
+ for (const handler of handlers.filter(it => it.code === event.code)) {
+ if (checkModifier(event, handler.modifiers) && checkState(event, handler.states)) {
+ handler.handler(event);
+ hit = true;
+ break;
+ }
+ }
+
+ if (!hit) {
+ for (const handler of handlers.filter(it => it.code === 'any')) {
+ handler.handler(event);
+ }
+ }
+}
diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts
index e20b23f166..54ec2ce39b 100644
--- a/packages/frontend/src/scripts/lookup.ts
+++ b/packages/frontend/src/scripts/lookup.ts
@@ -33,7 +33,43 @@ export async function lookup(router?: Router) {
uri: query,
});
- os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
+ os.promiseDialog(promise, null, (err) => {
+ let title = i18n.ts.somethingHappened;
+ let text = err.message + '\n' + err.id;
+
+ switch (err.id) {
+ case '974b799e-1a29-4889-b706-18d4dd93e266':
+ title = i18n.ts._remoteLookupErrors._federationNotAllowed.title;
+ text = i18n.ts._remoteLookupErrors._federationNotAllowed.description;
+ break;
+ case '1a5eab56-e47b-48c2-8d5e-217b897d70db':
+ title = i18n.ts._remoteLookupErrors._uriInvalid.title;
+ text = i18n.ts._remoteLookupErrors._uriInvalid.description;
+ break;
+ case '81b539cf-4f57-4b29-bc98-032c33c0792e':
+ title = i18n.ts._remoteLookupErrors._requestFailed.title;
+ text = i18n.ts._remoteLookupErrors._requestFailed.description;
+ break;
+ case '70193c39-54f3-4813-82f0-70a680f7495b':
+ title = i18n.ts._remoteLookupErrors._responseInvalid.title;
+ text = i18n.ts._remoteLookupErrors._responseInvalid.description;
+ break;
+ case 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a':
+ title = i18n.ts._remoteLookupErrors._responseInvalid.title;
+ text = i18n.ts._remoteLookupErrors._responseInvalidIdHostNotMatch.description;
+ break;
+ case 'dc94d745-1262-4e63-a17d-fecaa57efc82':
+ title = i18n.ts._remoteLookupErrors._noSuchObject.title;
+ text = i18n.ts._remoteLookupErrors._noSuchObject.description;
+ break;
+ }
+
+ os.alert({
+ type: 'error',
+ title,
+ text,
+ });
+ }, i18n.ts.fetchingAsApObject);
const res = await promise;
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts
index 89fdda0cbb..004b6d42a4 100644
--- a/packages/frontend/src/scripts/merge.ts
+++ b/packages/frontend/src/scripts/merge.ts
@@ -7,10 +7,10 @@ import { deepClone } from './clone.js';
import type { Cloneable } from './clone.js';
export type DeepPartial<T> = {
- [P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P];
+ [P in keyof T]?: T[P] extends Record<PropertyKey, unknown> ? DeepPartial<T[P]> : T[P];
};
-function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
+function isPureObject(value: unknown): value is Record<PropertyKey, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -18,14 +18,14 @@ function isPureObject(value: unknown): value is Record<string | number | symbol,
* valueにないキーをdefからもらう(再帰的)\
* nullはそのまま、undefinedはdefの値
**/
-export function deepMerge<X extends object>(value: DeepPartial<X>, def: X): X {
+export function deepMerge<X extends Record<PropertyKey, unknown>>(value: DeepPartial<X>, def: X): X {
if (isPureObject(value) && isPureObject(def)) {
const result = deepClone(value as Cloneable) as X;
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
result[k] = v;
} else if (isPureObject(v) && isPureObject(result[k])) {
- const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<string | number | symbol, unknown>>;
+ const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<PropertyKey, unknown>>;
result[k] = deepMerge<typeof v>(child, v);
}
}
diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts
index e7a92e2d5c..dc07ad477b 100644
--- a/packages/frontend/src/scripts/misskey-api.ts
+++ b/packages/frontend/src/scripts/misskey-api.ts
@@ -9,12 +9,24 @@ import { apiUrl } from '@@/js/config.js';
import { $i } from '@/account.js';
export const pendingApiRequestsCount = ref(0);
+export type Endpoint = keyof Misskey.Endpoints;
+
+export type Request<E extends Endpoint> = Misskey.Endpoints[E]['req'];
+
+export type AnyRequest<E extends Endpoint | (string & unknown)> =
+ (E extends Endpoint ? Request<E> : never) | object;
+
+export type Response<E extends Endpoint | (string & unknown), P extends AnyRequest<E>> =
+ E extends Endpoint
+ ? P extends Request<E> ? Misskey.api.SwitchCaseResponseType<E, P> : never
+ : object;
+
// Implements Misskey.api.ApiClient.request
export function misskeyApi<
ResT = void,
- E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
- P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
- _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
+ E extends Endpoint | NonNullable<string> = Endpoint,
+ P extends AnyRequest<E> = E extends Endpoint ? Request<E> : never,
+ _ResT = ResT extends void ? Response<E, P> : ResT,
>(
endpoint: E,
data: P & { i?: string | null; } = {} as any,
diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts
index 43dcf11936..a8a330eb6d 100644
--- a/packages/frontend/src/scripts/please-login.ts
+++ b/packages/frontend/src/scripts/please-login.ts
@@ -5,6 +5,7 @@
import { defineAsyncComponent } from 'vue';
import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { popup } from '@/os.js';
@@ -51,10 +52,17 @@ export function pleaseLogin(opts: {
} = {}) {
if ($i) return;
+ let _openOnRemote: OpenOnRemoteOptions | undefined = undefined;
+
+ // 連合できる場合と、(連合ができなくても)共有する場合は外部連携オプションを設定
+ if (opts.openOnRemote != null && (instance.federation !== 'none' || opts.openOnRemote.type === 'share')) {
+ _openOnRemote = opts.openOnRemote;
+ }
+
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
autoSet: true,
- message: opts.message ?? (opts.openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
- openOnRemote: opts.openOnRemote,
+ message: opts.message ?? (_openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
+ openOnRemote: _openOnRemote,
}, {
cancelled: () => {
if (opts.path) {
diff --git a/packages/frontend/src/scripts/sanitize-html.ts b/packages/frontend/src/scripts/sanitize-html.ts
index 6e1a46c746..fc9db9bbdb 100644
--- a/packages/frontend/src/scripts/sanitize-html.ts
+++ b/packages/frontend/src/scripts/sanitize-html.ts
@@ -13,6 +13,7 @@ export default function sanitizeHtml(str: string | null): string | null {
...original.defaults.allowedAttributes,
a: original.defaults.allowedAttributes.a.concat(['style']),
img: original.defaults.allowedAttributes.img.concat(['style']),
+ '*': (original.defaults.allowedAttributes['*'] || []).concat(['style']),
},
});
}
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index b037aa8acc..c25b4d73bd 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -12,14 +12,28 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { uploadFile } from '@/scripts/upload.js';
-export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise<Misskey.entities.DriveFile[]> {
+export function chooseFileFromPc(
+ multiple: boolean,
+ options?: {
+ uploadFolder?: string | null;
+ keepOriginal?: boolean;
+ nameConverter?: (file: File) => string | undefined;
+ },
+): Promise<Misskey.entities.DriveFile[]> {
+ const uploadFolder = options?.uploadFolder ?? defaultStore.state.uploadFolder;
+ const keepOriginal = options?.keepOriginal ?? defaultStore.state.keepOriginalUploading;
+ const nameConverter = options?.nameConverter ?? (() => undefined);
+
return new Promise((res, rej) => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.onchange = () => {
if (!input.files) return res([]);
- const promises = Array.from(input.files, file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal));
+ const promises = Array.from(
+ input.files,
+ file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal),
+ );
Promise.all(promises).then(driveFiles => {
res(driveFiles);
@@ -94,7 +108,7 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul
}, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
- action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)),
+ action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)),
}, {
text: i18n.ts.fromDrive,
icon: 'ti ti-cloud',
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index 05f82fce7d..2008afe045 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -93,6 +93,10 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ctx == null) {
ctx = new AudioContext();
+
+ window.addEventListener('beforeunload', () => {
+ ctx.close();
+ });
}
if (options?.useCache ?? true) {
if (cache.has(url)) {
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index 89aa023f23..d15d9043c2 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -7,7 +7,6 @@ import { onUnmounted, Ref, ShallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { useStream } from '@/stream.js';
import { $i } from '@/account.js';
-import * as os from '@/os.js';
import { misskeyApi } from './misskey-api.js';
export function useNoteCapture(props: {
diff --git a/packages/frontend/src/scripts/warning-external-website.ts b/packages/frontend/src/scripts/warning-external-website.ts
index 67158c6438..0c9b5ba806 100644
--- a/packages/frontend/src/scripts/warning-external-website.ts
+++ b/packages/frontend/src/scripts/warning-external-website.ts
@@ -8,13 +8,21 @@ import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import MkUrlWarningDialog from '@/components/MkUrlWarningDialog.vue';
-const extractDomain = /^(https?:\/\/|\/\/)?([^@/\s]+@)?(www\.)?([^:/\s]+)/i;
const isRegExp = /^\/(.+)\/(.*)$/;
+function extractHostname(maybeUrl: string): URL | null {
+ try {
+ const url = new URL(maybeUrl);
+ return url.host;
+ } catch {
+ return null;
+ }
+}
+
export async function warningExternalWebsite(url: string) {
- const domain = extractDomain.exec(url)?.[4];
+ const hostname = extractHostname(url);
- if (!domain) return false;
+ if (!hostname) return false;
const isTrustedByInstance = instance.trustedLinkUrlPatterns.some(expression => {
const r = isRegExp.exec(expression);
@@ -24,11 +32,11 @@ export async function warningExternalWebsite(url: string) {
} else if (expression.includes(' ')) {
return expression.split(' ').every(keyword => url.includes(keyword));
} else {
- return domain.endsWith(expression);
+ return `.${hostname}`.endsWith(`.${expression}`);
}
});
- const isTrustedByUser = defaultStore.reactiveState.trustedDomains.value.includes(domain);
+ const isTrustedByUser = defaultStore.reactiveState.trustedDomains.value.includes(hostname);
const isDisabledByUser = !defaultStore.reactiveState.warnExternalUrl.value;
if (!isTrustedByInstance && !isTrustedByUser && !isDisabledByUser) {
@@ -44,7 +52,7 @@ export async function warningExternalWebsite(url: string) {
});
if (confirm.canceled) return false;
-
+
return window.open(url, '_blank', 'nofollow noopener popup=false');
}
diff --git a/packages/frontend/src/server-context.ts b/packages/frontend/src/server-context.ts
index aa44a10290..e79d3fa314 100644
--- a/packages/frontend/src/server-context.ts
+++ b/packages/frontend/src/server-context.ts
@@ -2,22 +2,20 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
+
import * as Misskey from 'misskey-js';
-import { $i } from '@/account.js';
const providedContextEl = document.getElementById('misskey_clientCtx');
export type ServerContext = {
clip?: Misskey.entities.Clip;
note?: Misskey.entities.Note;
- user?: Misskey.entities.UserLite;
+ user?: Misskey.entities.UserDetailed;
} | null;
export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null;
-export function getServerContext<K extends keyof NonNullable<ServerContext>>(entity: K): Required<Pick<NonNullable<ServerContext>, K>> | null {
- // contextは非ログイン状態の情報しかないためログイン時は利用できない
- if ($i) return null;
-
- return serverContext ? (serverContext[entity] ?? null) : null;
+export function assertServerContext<K extends keyof NonNullable<ServerContext>>(ctx: ServerContext, entity: K): ctx is Required<Pick<NonNullable<ServerContext>, K>> {
+ if (ctx == null) return false;
+ return entity in ctx && ctx[entity] != null;
}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index c34e0bbf48..69fcef32c2 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -10,11 +10,11 @@ import lightTheme from '@@/themes/l-cherry.json5';
import darkTheme from '@@/themes/d-ice.json5';
import { searchEngineMap } from './scripts/search-engine-map.js';
import type { SoundType } from '@/scripts/sound.js';
+import type { Ast } from '@syuilo/aiscript';
import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js';
import { miLocalStorage } from '@/local-storage.js';
import { defaultFollowingFeedState } from '@/scripts/following-feed-utils.js';
import { Storage } from '@/pizzax.js';
-import type { Ast } from '@syuilo/aiscript';
interface PostFormAction {
title: string,
@@ -561,6 +561,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: true,
},
+ showSoftWordMutedWord: {
+ where: 'device',
+ default: false,
+ },
sound_masterVolume: {
where: 'device',
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index 8355ae3061..6d36df9874 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -56,12 +56,18 @@ export function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.customEmojis,
icon: 'ph-smiley ph-bold ph-lg',
to: '/about#emojis',
- }, {
- type: 'link',
- text: i18n.ts.federation,
- icon: 'ti ti-whirl',
- to: '/about#federation',
- }, {
+ });
+
+ if (instance.federation !== 'none') {
+ menuItems.push({
+ type: 'link',
+ text: i18n.ts.federation,
+ icon: 'ti ti-whirl',
+ to: '/about#federation',
+ });
+ }
+
+ menuItems.push({
type: 'link',
text: i18n.ts.charts,
icon: 'ti ti-chart-line',
@@ -134,7 +140,7 @@ export function openInstanceMenu(ev: MouseEvent) {
});
}
- if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) {
+ if (instance.impressumUrl != null || instance.tosUrl != null || instance.privacyPolicyUrl != null || instance.donationUrl != null) {
menuItems.push({ type: 'divider' });
}
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 5cc0e52f77..062a8faf3f 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</template>
<div :class="$style.divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
+ <MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
</MkA>
<button class="_button" :class="$style.item" @click="more">
@@ -48,10 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</div>
<div :class="$style.bottom">
- <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post">
+ <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="() => { os.post(); }">
<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
</button>
- <button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
+ <button v-if="$i != null" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
</button>
</div>
@@ -83,8 +83,12 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
+import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
-const iconOnly = ref(false);
+const forceIconOnly = ref(window.innerWidth <= 1279);
+const iconOnly = computed(() => {
+ return forceIconOnly.value || (defaultStore.reactiveState.menuDisplay.value === 'sideIcon');
+});
const menu = computed(() => defaultStore.state.menu);
const otherMenuItemIndicated = computed(() => {
@@ -95,14 +99,10 @@ const otherMenuItemIndicated = computed(() => {
return false;
});
-const forceIconOnly = window.innerWidth <= 1279;
-
function calcViewState() {
- iconOnly.value = forceIconOnly || (defaultStore.state.menuDisplay === 'sideIcon');
+ forceIconOnly.value = window.innerWidth <= 1279;
}
-calcViewState();
-
window.addEventListener('resize', calcViewState);
watch(defaultStore.reactiveState.menuDisplay, () => {
@@ -120,8 +120,10 @@ function openAccountMenu(ev: MouseEvent) {
}
function more(ev: MouseEvent) {
+ const target = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
+ if (!target) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
- src: ev.currentTarget ?? ev.target,
+ src: target,
}, {
closed: () => dispose(),
});
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index 5f9a938017..ed881bef22 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<span :class="$style.name">{{ x.name }}</span>
<XRss v-if="x.type === 'rss'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url" :shuffle="x.props.shuffle"/>
- <XFederation v-else-if="x.type === 'federation'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
+ <XFederation v-else-if="x.type === 'federation' && instance.federation !== 'none'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
<XUserList v-else-if="x.type === 'userList'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :userListId="x.props.userListId"/>
</div>
</div>
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
+import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js';
const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue'));
const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue'));
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index ded945dda1..f8cd5fa8be 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -260,6 +260,7 @@ onMounted(() => {
position: sticky;
top: 0;
height: 100%;
+ width: 300px;
padding-top: 16px;
box-sizing: border-box;
overflow: auto;
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index c0ea833546..36caca5fc0 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -100,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue';
import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue';
-import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
+import { deckStore, columnTypes, addColumn as addColumnToStore, forceSaveDeck, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
import type { ColumnType } from './deck/deck-store.js';
import type { MenuItem } from '@/types/menu.js';
import XSidebar from '@/ui/_common_/navbar.vue';
@@ -240,10 +240,15 @@ function changeProfile(ev: MouseEvent) {
title: i18n.ts._deck.profile,
minLength: 1,
});
+
if (canceled || name == null) return;
- deckStore.set('profile', name);
- unisonReload();
+ os.promiseDialog((async () => {
+ await deckStore.set('profile', name);
+ await forceSaveDeck();
+ })(), () => {
+ unisonReload();
+ });
},
});
}).then(() => {
@@ -258,9 +263,18 @@ async function deleteProfile() {
});
if (canceled) return;
- deleteProfile_(deckStore.state.profile);
- deckStore.set('profile', 'default');
- unisonReload();
+ os.promiseDialog((async () => {
+ if (deckStore.state.profile === 'default') {
+ await deckStore.set('columns', []);
+ await deckStore.set('layout', []);
+ await forceSaveDeck();
+ } else {
+ await deleteProfile_(deckStore.state.profile);
+ }
+ await deckStore.set('profile', 'default');
+ })(), () => {
+ unisonReload();
+ });
}
</script>
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index 91859b46d7..8e5b1dd1ac 100644
--- a/packages/frontend/src/ui/deck/deck-store.ts
+++ b/packages/frontend/src/ui/deck/deck-store.ts
@@ -113,8 +113,7 @@ export const loadDeck = async () => {
deckStore.set('layout', deck.layout);
};
-// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
-export const saveDeck = throttle(1000, async () => {
+export async function forceSaveDeck() {
await misskeyApi('i/registry/set', {
scope: ['client', 'deck', 'profiles'],
key: deckStore.state.profile,
@@ -123,6 +122,11 @@ export const saveDeck = throttle(1000, async () => {
layout: deckStore.reactiveState.layout.value,
},
});
+}
+
+// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
+export const saveDeck = throttle(1000, () => {
+ forceSaveDeck();
});
export async function getProfiles(): Promise<string[]> {
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index c552b65318..e8c71f61cf 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XWidgets/>
</div>
- <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
+ <button v-if="!isDesktop && !pageMetadata?.needWideArea && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
<div v-if="isMobile" ref="navFooter" :class="$style.nav">
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 92dc6d148e..3e43687709 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -70,7 +70,7 @@ const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries));
const fetching = ref(true);
const fetchEndpoint = computed(() => {
const url = new URL('/api/fetch-rss', base);
- url.searchParams.set('url', encodeURIComponent(widgetProps.url));
+ url.searchParams.set('url', widgetProps.url);
return url;
});
const intervalClear = ref<(() => void) | undefined>();
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 6957878572..4f594b720f 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -99,7 +99,7 @@ const items = computed(() => {
const fetching = ref(true);
const fetchEndpoint = computed(() => {
const url = new URL('/api/fetch-rss', base);
- url.searchParams.set('url', encodeURIComponent(widgetProps.url));
+ url.searchParams.set('url', widgetProps.url);
return url;
});
const intervalClear = ref<(() => void) | undefined>();
diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts
index 29e4558f1e..37742f8c2e 100644
--- a/packages/frontend/src/widgets/index.ts
+++ b/packages/frontend/src/widgets/index.ts
@@ -37,6 +37,12 @@ export default function(app: App) {
app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue')));
}
+// 連合関連のウィジェット(連合無効時に隠す)
+export const federationWidgets = [
+ 'federation',
+ 'instanceCloud',
+];
+
export const widgets = [
'profile',
'instanceInfo',
@@ -52,8 +58,6 @@ export const widgets = [
'photos',
'digitalClock',
'unixClock',
- 'federation',
- 'instanceCloud',
'postForm',
'slideshow',
'serverMetric',
@@ -67,4 +71,6 @@ export const widgets = [
'clicker',
'search',
'birthdayFollowings',
+
+ ...federationWidgets,
];
diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts
new file mode 100644
index 0000000000..2a15a74249
--- /dev/null
+++ b/packages/frontend/test/aiscript/api.test.ts
@@ -0,0 +1,401 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { miLocalStorage } from '@/local-storage.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
+import {
+ afterAll,
+ afterEach,
+ beforeAll,
+ beforeEach,
+ describe,
+ expect,
+ test,
+ vi
+} from 'vitest';
+
+async function exe(script: string): Promise<values.Value[]> {
+ const outputs: values.Value[] = [];
+ const interpreter = new Interpreter(
+ createAiScriptEnv({ storageKey: 'widget' }),
+ {
+ in: aiScriptReadline,
+ out: (value) => {
+ outputs.push(value);
+ }
+ }
+ );
+ const ast = Parser.parse(script);
+ await interpreter.exec(ast);
+ return outputs;
+}
+
+let $iMock = vi.hoisted<Partial<typeof import('@/account.js').$i> | null >(
+ () => null
+);
+
+vi.mock('@/account.js', () => {
+ return {
+ get $i() {
+ return $iMock;
+ },
+ };
+});
+
+const osMock = vi.hoisted(() => {
+ return {
+ inputText: vi.fn(),
+ alert: vi.fn(),
+ confirm: vi.fn(),
+ };
+});
+
+vi.mock('@/os.js', () => {
+ return osMock;
+});
+
+const misskeyApiMock = vi.hoisted(() => vi.fn());
+
+vi.mock('@/scripts/misskey-api.js', () => {
+ return { misskeyApi: misskeyApiMock };
+});
+
+describe('AiScript common API', () => {
+ afterAll(() => {
+ vi.unstubAllGlobals();
+ });
+
+ describe('readline', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ test.sequential('ok', async () => {
+ osMock.inputText.mockImplementationOnce(async ({ title }) => {
+ expect(title).toBe('question');
+ return {
+ canceled: false,
+ result: 'Hello',
+ };
+ });
+ const [res] = await exe(`
+ <: readline('question')
+ `);
+ expect(res).toStrictEqual(values.STR('Hello'));
+ expect(osMock.inputText).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('cancelled', async () => {
+ osMock.inputText.mockImplementationOnce(async ({ title }) => {
+ expect(title).toBe('question');
+ return {
+ canceled: true,
+ result: undefined,
+ };
+ });
+ const [res] = await exe(`
+ <: readline('question')
+ `);
+ expect(res).toStrictEqual(values.STR(''));
+ expect(osMock.inputText).toHaveBeenCalledOnce();
+ });
+ });
+
+ describe('user constants', () => {
+ describe.sequential('logged in', () => {
+ beforeAll(() => {
+ $iMock = {
+ id: 'xxxxxxxx',
+ name: '藍',
+ username: 'ai',
+ };
+ });
+
+ test.concurrent('USER_ID', async () => {
+ const [res] = await exe(`
+ <: USER_ID
+ `);
+ expect(res).toStrictEqual(values.STR('xxxxxxxx'));
+ });
+
+ test.concurrent('USER_NAME', async () => {
+ const [res] = await exe(`
+ <: USER_NAME
+ `);
+ expect(res).toStrictEqual(values.STR('藍'));
+ });
+
+ test.concurrent('USER_USERNAME', async () => {
+ const [res] = await exe(`
+ <: USER_USERNAME
+ `);
+ expect(res).toStrictEqual(values.STR('ai'));
+ });
+ });
+
+ describe.sequential('not logged in', () => {
+ beforeAll(() => {
+ $iMock = null;
+ });
+
+ test.concurrent('USER_ID', async () => {
+ const [res] = await exe(`
+ <: USER_ID
+ `);
+ expect(res).toStrictEqual(values.NULL);
+ });
+
+ test.concurrent('USER_NAME', async () => {
+ const [res] = await exe(`
+ <: USER_NAME
+ `);
+ expect(res).toStrictEqual(values.NULL);
+ });
+
+ test.concurrent('USER_USERNAME', async () => {
+ const [res] = await exe(`
+ <: USER_USERNAME
+ `);
+ expect(res).toStrictEqual(values.NULL);
+ });
+ });
+ });
+
+ describe('dialog', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ test.sequential('ok', async () => {
+ osMock.alert.mockImplementationOnce(async ({ type, title, text }) => {
+ expect(type).toBe('success');
+ expect(title).toBe('Hello');
+ expect(text).toBe('world');
+ });
+ const [res] = await exe(`
+ <: Mk:dialog('Hello', 'world', 'success')
+ `);
+ expect(res).toStrictEqual(values.NULL);
+ expect(osMock.alert).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('omit type', async () => {
+ osMock.alert.mockImplementationOnce(async ({ type, title, text }) => {
+ expect(type).toBe('info');
+ expect(title).toBe('Hello');
+ expect(text).toBe('world');
+ });
+ const [res] = await exe(`
+ <: Mk:dialog('Hello', 'world')
+ `);
+ expect(res).toStrictEqual(values.NULL);
+ expect(osMock.alert).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('invalid type', async () => {
+ await expect(() => exe(`
+ <: Mk:dialog('Hello', 'world', 'invalid')
+ `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
+ expect(osMock.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('confirm', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ test.sequential('ok', async () => {
+ osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => {
+ expect(type).toBe('success');
+ expect(title).toBe('Hello');
+ expect(text).toBe('world');
+ return { canceled: false };
+ });
+ const [res] = await exe(`
+ <: Mk:confirm('Hello', 'world', 'success')
+ `);
+ expect(res).toStrictEqual(values.TRUE);
+ expect(osMock.confirm).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('omit type', async () => {
+ osMock.confirm
+ .mockImplementationOnce(async ({ type, title, text }) => {
+ expect(type).toBe('question');
+ expect(title).toBe('Hello');
+ expect(text).toBe('world');
+ return { canceled: false };
+ });
+ const [res] = await exe(`
+ <: Mk:confirm('Hello', 'world')
+ `);
+ expect(res).toStrictEqual(values.TRUE);
+ expect(osMock.confirm).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('canceled', async () => {
+ osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => {
+ expect(type).toBe('question');
+ expect(title).toBe('Hello');
+ expect(text).toBe('world');
+ return { canceled: true };
+ });
+ const [res] = await exe(`
+ <: Mk:confirm('Hello', 'world')
+ `);
+ expect(res).toStrictEqual(values.FALSE);
+ expect(osMock.confirm).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('invalid type', async () => {
+ const confirm = osMock.confirm;
+ await expect(() => exe(`
+ <: Mk:confirm('Hello', 'world', 'invalid')
+ `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
+ expect(confirm).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('api', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ test.sequential('successful', async () => {
+ misskeyApiMock.mockImplementationOnce(
+ async (endpoint, data, token) => {
+ expect(endpoint).toBe('ping');
+ expect(data).toStrictEqual({});
+ expect(token).toBeNull();
+ return { pong: 1735657200000 };
+ }
+ );
+ const [res] = await exe(`
+ <: Mk:api('ping', {})
+ `);
+ expect(res).toStrictEqual(values.OBJ(new Map([
+ ['pong', values.NUM(1735657200000)],
+ ])));
+ expect(misskeyApiMock).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('with token', async () => {
+ misskeyApiMock.mockImplementationOnce(
+ async (endpoint, data, token) => {
+ expect(endpoint).toBe('ping');
+ expect(data).toStrictEqual({});
+ expect(token).toStrictEqual('xxxxxxxx');
+ return { pong: 1735657200000 };
+ }
+ );
+ const [res] = await exe(`
+ <: Mk:api('ping', {}, 'xxxxxxxx')
+ `);
+ expect(res).toStrictEqual(values.OBJ(new Map([
+ ['pong', values.NUM(1735657200000 )],
+ ])));
+ expect(misskeyApiMock).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('request failed', async () => {
+ misskeyApiMock.mockRejectedValueOnce('Not Found');
+ const [res] = await exe(`
+ <: Mk:api('this/endpoint/should/not/be/found', {})
+ `);
+ expect(res).toStrictEqual(
+ values.ERROR('request_failed', values.STR('Not Found'))
+ );
+ expect(misskeyApiMock).toHaveBeenCalledOnce();
+ });
+
+ test.sequential('invalid endpoint', async () => {
+ await expect(() => exe(`
+ Mk:api('https://example.com/api/ping', {})
+ `)).rejects.toStrictEqual(
+ new errors.AiScriptRuntimeError('invalid endpoint'),
+ );
+ expect(misskeyApiMock).not.toHaveBeenCalled();
+ });
+
+ test.sequential('missing param', async () => {
+ await expect(() => exe(`
+ Mk:api('ping')
+ `)).rejects.toStrictEqual(
+ new errors.AiScriptRuntimeError('expected param'),
+ );
+ expect(misskeyApiMock).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('save and load', () => {
+ beforeEach(() => {
+ miLocalStorage.removeItem('aiscript:widget:key');
+ });
+
+ afterEach(() => {
+ miLocalStorage.removeItem('aiscript:widget:key');
+ });
+
+ test.sequential('successful', async () => {
+ const [res] = await exe(`
+ Mk:save('key', 'value')
+ <: Mk:load('key')
+ `);
+ expect(miLocalStorage.getItem('aiscript:widget:key')).toBe('"value"');
+ expect(res).toStrictEqual(values.STR('value'));
+ });
+
+ test.sequential('missing value to save', async () => {
+ await expect(() => exe(`
+ Mk:save('key')
+ `)).rejects.toStrictEqual(
+ new errors.AiScriptRuntimeError('Expect anything, but got nothing.'),
+ );
+ });
+
+ test.sequential('not value found to load', async () => {
+ const [res] = await exe(`
+ <: Mk:load('key')
+ `);
+ expect(res).toStrictEqual(values.NULL);
+ });
+
+ test.sequential('remove existing', async () => {
+ const res = await exe(`
+ Mk:save('key', 'value')
+ <: Mk:load('key')
+ <: Mk:remove('key')
+ <: Mk:load('key')
+ `);
+ expect(res).toStrictEqual([values.STR('value'), values.NULL, values.NULL]);
+ });
+
+ test.sequential('remove nothing', async () => {
+ const res = await exe(`
+ <: Mk:load('key')
+ <: Mk:remove('key')
+ <: Mk:load('key')
+ `);
+ expect(res).toStrictEqual([values.NULL, values.NULL, values.NULL]);
+ });
+ });
+
+ test.concurrent('url', async () => {
+ vi.stubGlobal('location', { href: 'https://example.com/' });
+ const [res] = await exe(`
+ <: Mk:url()
+ `);
+ expect(res).toStrictEqual(values.STR('https://example.com/'));
+ });
+
+ test.concurrent('nyaize', async () => {
+ const [res] = await exe(`
+ <: Mk:nyaize('な')
+ `);
+ expect(res).toStrictEqual(values.STR('にゃ'));
+ });
+});
diff --git a/packages/frontend/test/aiscript/common.test.ts b/packages/frontend/test/aiscript/common.test.ts
new file mode 100644
index 0000000000..acc48826ea
--- /dev/null
+++ b/packages/frontend/test/aiscript/common.test.ts
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { assertStringAndIsIn } from "@/scripts/aiscript/common.js";
+import { values } from "@syuilo/aiscript";
+import { describe, expect, test } from "vitest";
+
+describe('AiScript common script', () => {
+ test('assertStringAndIsIn', () => {
+ expect(
+ () => assertStringAndIsIn(values.STR('a'), ['a', 'b'])
+ ).not.toThrow();
+ expect(
+ () => assertStringAndIsIn(values.STR('c'), ['a', 'b'])
+ ).toThrow('"c" is not in "a", "b"');
+ expect(() => assertStringAndIsIn(
+ values.STR('invalid'),
+ ['left', 'center', 'right']
+ )).toThrow('"invalid" is not in "left", "center", "right"');
+ });
+});
diff --git a/packages/frontend/test/aiscript/ui.test.ts b/packages/frontend/test/aiscript/ui.test.ts
new file mode 100644
index 0000000000..d3f930173f
--- /dev/null
+++ b/packages/frontend/test/aiscript/ui.test.ts
@@ -0,0 +1,826 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
+import { describe, expect, test } from 'vitest';
+import { type Ref, ref } from 'vue';
+import type {
+ AsUiButton,
+ AsUiButtons,
+ AsUiComponent,
+ AsUiMfm,
+ AsUiNumberInput,
+ AsUiRoot,
+ AsUiSelect,
+ AsUiSwitch,
+ AsUiText,
+ AsUiTextarea,
+ AsUiTextInput,
+} from '@/scripts/aiscript/ui.js';
+import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
+
+type ExeResult = {
+ root: AsUiRoot;
+ get: (id: string) => AsUiComponent;
+ outputs: values.Value[];
+}
+
+async function exe(script: string): Promise<ExeResult> {
+ const rootRef = ref<AsUiRoot>();
+ const componentRefs = ref<Ref<AsUiComponent>[]>([]);
+ const outputs: values.Value[] = [];
+
+ const interpreter = new Interpreter(
+ registerAsUiLib(componentRefs.value, (root) => {
+ rootRef.value = root.value;
+ }),
+ {
+ out: (value) => {
+ outputs.push(value);
+ }
+ }
+ );
+ const ast = Parser.parse(script);
+ await interpreter.exec(ast);
+
+ const root = rootRef.value;
+ if (root === undefined) {
+ expect.unreachable('root must not be undefined');
+ }
+ const components = componentRefs.value.map(
+ (componentRef) => componentRef.value,
+ );
+ expect(root).toBe(components[0]);
+ expect(root.type).toBe('root');
+ const get = (id: string) => {
+ const component = componentRefs.value.find(
+ (componentRef) => componentRef.value.id === id,
+ );
+ if (component === undefined) {
+ expect.unreachable(`component "${id}" is not defined`);
+ }
+ return component.value;
+ };
+ return { root, get, outputs };
+}
+
+describe('AiScript UI API', () => {
+ test.concurrent('root', async () => {
+ const { root } = await exe('');
+ expect(root.children).toStrictEqual([]);
+ });
+
+ describe('get', () => {
+ test.concurrent('some', async () => {
+ const { outputs } = await exe(`
+ Ui:C:text({}, 'id')
+ <: Ui:get('id')
+ `);
+ const output = outputs[0] as values.VObj;
+ expect(output.type).toBe('obj');
+ expect(output.value.size).toBe(2);
+ expect(output.value.get('id')).toStrictEqual(values.STR('id'));
+ expect(output.value.get('update')!.type).toBe('fn');
+ });
+
+ test.concurrent('none', async () => {
+ const { outputs } = await exe(`
+ <: Ui:get('id')
+ `);
+ expect(outputs).toStrictEqual([values.NULL]);
+ });
+ });
+
+ describe('update', () => {
+ test.concurrent('normal', async () => {
+ const { get } = await exe(`
+ let text = Ui:C:text({ text: 'a' }, 'id')
+ text.update({ text: 'b' })
+ `);
+ const text = get('id') as AsUiText;
+ expect(text.text).toBe('b');
+ });
+
+ test.concurrent('skip unknown key', async () => {
+ const { get } = await exe(`
+ let text = Ui:C:text({ text: 'a' }, 'id')
+ text.update({
+ text: 'b'
+ unknown: null
+ })
+ `);
+ const text = get('id') as AsUiText;
+ expect(text.text).toBe('b');
+ expect('unknown' in text).toBeFalsy();
+ });
+ });
+
+ describe('container', () => {
+ test.concurrent('all options', async () => {
+ const { root, get } = await exe(`
+ let text = Ui:C:text({
+ text: 'text'
+ }, 'id1')
+ let container = Ui:C:container({
+ children: [text]
+ align: 'left'
+ bgColor: '#fff'
+ fgColor: '#000'
+ font: 'sans-serif'
+ borderWidth: 1
+ borderColor: '#f00'
+ borderStyle: 'hidden'
+ borderRadius: 2
+ padding: 3
+ rounded: true
+ hidden: false
+ }, 'id2')
+ Ui:render([container])
+ `);
+ expect(root.children).toStrictEqual(['id2']);
+ expect(get('id2')).toStrictEqual({
+ type: 'container',
+ id: 'id2',
+ children: ['id1'],
+ align: 'left',
+ bgColor: '#fff',
+ fgColor: '#000',
+ font: 'sans-serif',
+ borderColor: '#f00',
+ borderWidth: 1,
+ borderStyle: 'hidden',
+ borderRadius: 2,
+ padding: 3,
+ rounded: true,
+ hidden: false,
+ });
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:container({}, 'id')
+ `);
+ expect(get('id')).toStrictEqual({
+ type: 'container',
+ id: 'id',
+ children: [],
+ align: undefined,
+ fgColor: undefined,
+ bgColor: undefined,
+ font: undefined,
+ borderWidth: undefined,
+ borderColor: undefined,
+ borderStyle: undefined,
+ borderRadius: undefined,
+ padding: undefined,
+ rounded: undefined,
+ hidden: undefined,
+ });
+ });
+
+ test.concurrent('invalid children', async () => {
+ await expect(() => exe(`
+ Ui:C:container({
+ children: 0
+ })
+ `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
+ });
+
+ test.concurrent('invalid align', async () => {
+ await expect(() => exe(`
+ Ui:C:container({
+ align: 'invalid'
+ })
+ `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
+ });
+
+ test.concurrent('invalid font', async () => {
+ await expect(() => exe(`
+ Ui:C:container({
+ font: 'invalid'
+ })
+ `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
+ });
+
+ test.concurrent('invalid borderStyle', async () => {
+ await expect(() => exe(`
+ Ui:C:container({
+ borderStyle: 'invalid'
+ })
+ `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
+ });
+ });
+
+ describe('text', () => {
+ test.concurrent('all options', async () => {
+ const { root, get } = await exe(`
+ let text = Ui:C:text({
+ text: 'a'
+ size: 1
+ bold: true
+ color: '#000'
+ font: 'sans-serif'
+ }, 'id')
+ Ui:render([text])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ expect(get('id')).toStrictEqual({
+ type: 'text',
+ id: 'id',
+ text: 'a',
+ size: 1,
+ bold: true,
+ color: '#000',
+ font: 'sans-serif',
+ });
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:text({}, 'id')
+ `);
+ expect(get('id')).toStrictEqual({
+ type: 'text',
+ id: 'id',
+ text: undefined,
+ size: undefined,
+ bold: undefined,
+ color: undefined,
+ font: undefined,
+ });
+ });
+
+ test.concurrent('invalid font', async () => {
+ await expect(() => exe(`
+ Ui:C:text({
+ font: 'invalid'
+ })
+ `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
+ });
+ });
+
+ describe('mfm', () => {
+ test.concurrent('all options', async () => {
+ const { root, get, outputs } = await exe(`
+ let mfm = Ui:C:mfm({
+ text: 'text'
+ size: 1
+ bold: true
+ color: '#000'
+ font: 'sans-serif'
+ onClickEv: print
+ }, 'id')
+ Ui:render([mfm])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ const { onClickEv, ...mfm } = get('id') as AsUiMfm;
+ expect(mfm).toStrictEqual({
+ type: 'mfm',
+ id: 'id',
+ text: 'text',
+ size: 1,
+ bold: true,
+ color: '#000',
+ font: 'sans-serif',
+ });
+ await onClickEv!('a');
+ expect(outputs).toStrictEqual([values.STR('a')]);
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:mfm({}, 'id')
+ `);
+ const { onClickEv, ...mfm } = get('id') as AsUiMfm;
+ expect(onClickEv).toBeTypeOf('function');
+ expect(mfm).toStrictEqual({
+ type: 'mfm',
+ id: 'id',
+ text: undefined,
+ size: undefined,
+ bold: undefined,
+ color: undefined,
+ font: undefined,
+ });
+ });
+
+ test.concurrent('invalid font', async () => {
+ await expect(() => exe(`
+ Ui:C:mfm({
+ font: 'invalid'
+ })
+ `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
+ });
+ });
+
+ describe('textInput', () => {
+ test.concurrent('all options', async () => {
+ const { root, get, outputs } = await exe(`
+ let text_input = Ui:C:textInput({
+ onInput: print
+ default: 'a'
+ label: 'b'
+ caption: 'c'
+ }, 'id')
+ Ui:render([text_input])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ const { onInput, ...textInput } = get('id') as AsUiTextInput;
+ expect(textInput).toStrictEqual({
+ type: 'textInput',
+ id: 'id',
+ default: 'a',
+ label: 'b',
+ caption: 'c',
+ });
+ await onInput!('d');
+ expect(outputs).toStrictEqual([values.STR('d')]);
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:textInput({}, 'id')
+ `);
+ const { onInput, ...textInput } = get('id') as AsUiTextInput;
+ expect(onInput).toBeTypeOf('function');
+ expect(textInput).toStrictEqual({
+ type: 'textInput',
+ id: 'id',
+ default: undefined,
+ label: undefined,
+ caption: undefined,
+ });
+ });
+ });
+
+ describe('textarea', () => {
+ test.concurrent('all options', async () => {
+ const { root, get, outputs } = await exe(`
+ let textarea = Ui:C:textarea({
+ onInput: print
+ default: 'a'
+ label: 'b'
+ caption: 'c'
+ }, 'id')
+ Ui:render([textarea])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ const { onInput, ...textarea } = get('id') as AsUiTextarea;
+ expect(textarea).toStrictEqual({
+ type: 'textarea',
+ id: 'id',
+ default: 'a',
+ label: 'b',
+ caption: 'c',
+ });
+ await onInput!('d');
+ expect(outputs).toStrictEqual([values.STR('d')]);
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:textarea({}, 'id')
+ `);
+ const { onInput, ...textarea } = get('id') as AsUiTextarea;
+ expect(onInput).toBeTypeOf('function');
+ expect(textarea).toStrictEqual({
+ type: 'textarea',
+ id: 'id',
+ default: undefined,
+ label: undefined,
+ caption: undefined,
+ });
+ });
+ });
+
+ describe('numberInput', () => {
+ test.concurrent('all options', async () => {
+ const { root, get, outputs } = await exe(`
+ let number_input = Ui:C:numberInput({
+ onInput: print
+ default: 1
+ label: 'a'
+ caption: 'b'
+ }, 'id')
+ Ui:render([number_input])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ const { onInput, ...numberInput } = get('id') as AsUiNumberInput;
+ expect(numberInput).toStrictEqual({
+ type: 'numberInput',
+ id: 'id',
+ default: 1,
+ label: 'a',
+ caption: 'b',
+ });
+ await onInput!(2);
+ expect(outputs).toStrictEqual([values.NUM(2)]);
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:numberInput({}, 'id')
+ `);
+ const { onInput, ...numberInput } = get('id') as AsUiNumberInput;
+ expect(onInput).toBeTypeOf('function');
+ expect(numberInput).toStrictEqual({
+ type: 'numberInput',
+ id: 'id',
+ default: undefined,
+ label: undefined,
+ caption: undefined,
+ });
+ });
+ });
+
+ describe('button', () => {
+ test.concurrent('all options', async () => {
+ const { root, get, outputs } = await exe(`
+ let button = Ui:C:button({
+ text: 'a'
+ onClick: @() { <: 'clicked' }
+ primary: true
+ rounded: false
+ disabled: false
+ }, 'id')
+ Ui:render([button])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ const { onClick, ...button } = get('id') as AsUiButton;
+ expect(button).toStrictEqual({
+ type: 'button',
+ id: 'id',
+ text: 'a',
+ primary: true,
+ rounded: false,
+ disabled: false,
+ });
+ await onClick!();
+ expect(outputs).toStrictEqual([values.STR('clicked')]);
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:button({}, 'id')
+ `);
+ const { onClick, ...button } = get('id') as AsUiButton;
+ expect(onClick).toBeTypeOf('function');
+ expect(button).toStrictEqual({
+ type: 'button',
+ id: 'id',
+ text: undefined,
+ primary: undefined,
+ rounded: undefined,
+ disabled: undefined,
+ });
+ });
+ });
+
+ describe('buttons', () => {
+ test.concurrent('all options', async () => {
+ const { root, get } = await exe(`
+ let buttons = Ui:C:buttons({
+ buttons: []
+ }, 'id')
+ Ui:render([buttons])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ expect(get('id')).toStrictEqual({
+ type: 'buttons',
+ id: 'id',
+ buttons: [],
+ });
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:buttons({}, 'id')
+ `);
+ expect(get('id')).toStrictEqual({
+ type: 'buttons',
+ id: 'id',
+ buttons: [],
+ });
+ });
+
+ test.concurrent('some buttons', async () => {
+ const { root, get, outputs } = await exe(`
+ let buttons = Ui:C:buttons({
+ buttons: [
+ {
+ text: 'a'
+ onClick: @() { <: 'clicked a' }
+ primary: true
+ rounded: false
+ disabled: false
+ }
+ {
+ text: 'b'
+ onClick: @() { <: 'clicked b' }
+ primary: true
+ rounded: false
+ disabled: false
+ }
+ ]
+ }, 'id')
+ Ui:render([buttons])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ const { buttons, ...buttonsOptions } = get('id') as AsUiButtons;
+ expect(buttonsOptions).toStrictEqual({
+ type: 'buttons',
+ id: 'id',
+ });
+ expect(buttons!.length).toBe(2);
+ const { onClick: onClickA, ...buttonA } = buttons![0];
+ expect(buttonA).toStrictEqual({
+ text: 'a',
+ primary: true,
+ rounded: false,
+ disabled: false,
+ });
+ const { onClick: onClickB, ...buttonB } = buttons![1];
+ expect(buttonB).toStrictEqual({
+ text: 'b',
+ primary: true,
+ rounded: false,
+ disabled: false,
+ });
+ await onClickA!();
+ await onClickB!();
+ expect(outputs).toStrictEqual(
+ [values.STR('clicked a'), values.STR('clicked b')]
+ );
+ });
+ });
+
+ describe('switch', () => {
+ test.concurrent('all options', async () => {
+ const { root, get, outputs } = await exe(`
+ let switch = Ui:C:switch({
+ onChange: print
+ default: false
+ label: 'a'
+ caption: 'b'
+ }, 'id')
+ Ui:render([switch])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ const { onChange, ...switchOptions } = get('id') as AsUiSwitch;
+ expect(switchOptions).toStrictEqual({
+ type: 'switch',
+ id: 'id',
+ default: false,
+ label: 'a',
+ caption: 'b',
+ });
+ await onChange!(true);
+ expect(outputs).toStrictEqual([values.TRUE]);
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:switch({}, 'id')
+ `);
+ const { onChange, ...switchOptions } = get('id') as AsUiSwitch;
+ expect(onChange).toBeTypeOf('function');
+ expect(switchOptions).toStrictEqual({
+ type: 'switch',
+ id: 'id',
+ default: undefined,
+ label: undefined,
+ caption: undefined,
+ });
+ });
+ });
+
+ describe('select', () => {
+ test.concurrent('all options', async () => {
+ const { root, get, outputs } = await exe(`
+ let select = Ui:C:select({
+ items: [
+ { text: 'A', value: 'a' }
+ { text: 'B', value: 'b' }
+ ]
+ onChange: print
+ default: 'a'
+ label: 'c'
+ caption: 'd'
+ }, 'id')
+ Ui:render([select])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ const { onChange, ...select } = get('id') as AsUiSelect;
+ expect(select).toStrictEqual({
+ type: 'select',
+ id: 'id',
+ items: [
+ { text: 'A', value: 'a' },
+ { text: 'B', value: 'b' },
+ ],
+ default: 'a',
+ label: 'c',
+ caption: 'd',
+ });
+ await onChange!('b');
+ expect(outputs).toStrictEqual([values.STR('b')]);
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:select({}, 'id')
+ `);
+ const { onChange, ...select } = get('id') as AsUiSelect;
+ expect(onChange).toBeTypeOf('function');
+ expect(select).toStrictEqual({
+ type: 'select',
+ id: 'id',
+ items: [],
+ default: undefined,
+ label: undefined,
+ caption: undefined,
+ });
+ });
+
+ test.concurrent('omit item values', async () => {
+ const { get } = await exe(`
+ let select = Ui:C:select({
+ items: [
+ { text: 'A' }
+ { text: 'B' }
+ ]
+ }, 'id')
+ `);
+ const { onChange, ...select } = get('id') as AsUiSelect;
+ expect(onChange).toBeTypeOf('function');
+ expect(select).toStrictEqual({
+ type: 'select',
+ id: 'id',
+ items: [
+ { text: 'A', value: 'A' },
+ { text: 'B', value: 'B' },
+ ],
+ default: undefined,
+ label: undefined,
+ caption: undefined,
+ });
+ });
+ });
+
+ describe('folder', () => {
+ test.concurrent('all options', async () => {
+ const { root, get } = await exe(`
+ let folder = Ui:C:folder({
+ children: []
+ title: 'a'
+ opened: true
+ }, 'id')
+ Ui:render([folder])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ expect(get('id')).toStrictEqual({
+ type: 'folder',
+ id: 'id',
+ children: [],
+ title: 'a',
+ opened: true,
+ });
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:folder({}, 'id')
+ `);
+ expect(get('id')).toStrictEqual({
+ type: 'folder',
+ id: 'id',
+ children: [],
+ title: '',
+ opened: true,
+ });
+ });
+
+ test.concurrent('some children', async () => {
+ const { get } = await exe(`
+ let text = Ui:C:text({
+ text: 'text'
+ }, 'id1')
+ Ui:C:folder({
+ children: [text]
+ }, 'id2')
+ `);
+ expect(get('id2')).toStrictEqual({
+ type: 'folder',
+ id: 'id2',
+ children: ['id1'],
+ title: '',
+ opened: true,
+ });
+ });
+ });
+
+ describe('postFormButton', () => {
+ test.concurrent('all options', async () => {
+ const { root, get } = await exe(`
+ let post_form_button = Ui:C:postFormButton({
+ text: 'a'
+ primary: true
+ rounded: false
+ form: {
+ text: 'b'
+ cw: 'c'
+ visibility: 'public'
+ localOnly: true
+ }
+ }, 'id')
+ Ui:render([post_form_button])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ expect(get('id')).toStrictEqual({
+ type: 'postFormButton',
+ id: 'id',
+ text: 'a',
+ primary: true,
+ rounded: false,
+ form: {
+ text: 'b',
+ cw: 'c',
+ visibility: 'public',
+ localOnly: true,
+ },
+ });
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:postFormButton({}, 'id')
+ `);
+ expect(get('id')).toStrictEqual({
+ type: 'postFormButton',
+ id: 'id',
+ text: undefined,
+ primary: undefined,
+ rounded: undefined,
+ form: { text: '' },
+ });
+ });
+ });
+
+ describe('postForm', () => {
+ test.concurrent('all options', async () => {
+ const { root, get } = await exe(`
+ let post_form = Ui:C:postForm({
+ form: {
+ text: 'a'
+ cw: 'b'
+ visibility: 'public'
+ localOnly: true
+ }
+ }, 'id')
+ Ui:render([post_form])
+ `);
+ expect(root.children).toStrictEqual(['id']);
+ expect(get('id')).toStrictEqual({
+ type: 'postForm',
+ id: 'id',
+ form: {
+ text: 'a',
+ cw: 'b',
+ visibility: 'public',
+ localOnly: true,
+ },
+ });
+ });
+
+ test.concurrent('minimum options', async () => {
+ const { get } = await exe(`
+ Ui:C:postForm({}, 'id')
+ `);
+ expect(get('id')).toStrictEqual({
+ type: 'postForm',
+ id: 'id',
+ form: { text: '' },
+ });
+ });
+
+ test.concurrent('minimum options for form', async () => {
+ const { get } = await exe(`
+ Ui:C:postForm({
+ form: { text: '' }
+ }, 'id')
+ `);
+ expect(get('id')).toStrictEqual({
+ type: 'postForm',
+ id: 'id',
+ form: {
+ text: '',
+ cw: undefined,
+ visibility: undefined,
+ localOnly: undefined,
+ },
+ });
+ });
+ });
+});
diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json
index bbf9d653cf..169bd5029c 100644
--- a/packages/frontend/tsconfig.json
+++ b/packages/frontend/tsconfig.json
@@ -10,8 +10,8 @@
"declaration": false,
"sourceMap": false,
"target": "ES2022",
- "module": "nodenext",
- "moduleResolution": "nodenext",
+ "module": "ES2022",
+ "moduleResolution": "Bundler",
"removeComments": false,
"noLib": false,
"strict": true,
diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts
deleted file mode 100644
index d588f83138..0000000000
--- a/packages/frontend/vite.config.local-dev.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import dns from 'dns';
-import { readFile } from 'node:fs/promises';
-import type { IncomingMessage } from 'node:http';
-import { defineConfig } from 'vite';
-import type { UserConfig } from 'vite';
-import * as yaml from 'js-yaml';
-import locales from '../../locales/index.js';
-import { getConfig } from './vite.config.js';
-
-dns.setDefaultResultOrder('ipv4first');
-
-const defaultConfig = getConfig();
-
-const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
-
-const httpUrl = `http://localhost:${port}/`;
-const websocketUrl = `ws://localhost:${port}/`;
-const embedUrl = `http://localhost:5174/`;
-
-// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す
-function varyHandler(req: IncomingMessage) {
- if (req.headers.accept?.includes('application/activity+json')) {
- return null;
- }
- return '/index.html';
-}
-
-const devConfig: UserConfig = {
- // 基本の設定は vite.config.js から引き継ぐ
- ...defaultConfig,
- root: 'src',
- publicDir: '../assets',
- base: './',
- server: {
- host: 'localhost',
- port: 5173,
- proxy: {
- '/api': {
- changeOrigin: true,
- target: httpUrl,
- },
- '/assets': httpUrl,
- '/static-assets': httpUrl,
- '/client-assets': httpUrl,
- '/files': httpUrl,
- '/twemoji': httpUrl,
- '/fluent-emoji': httpUrl,
- '/tossface': httpUrl,
- '/sw.js': httpUrl,
- '/streaming': {
- target: websocketUrl,
- ws: true,
- },
- '/favicon.ico': httpUrl,
- '/robots.txt': httpUrl,
- '/embed.js': httpUrl,
- '/embed': {
- target: embedUrl,
- ws: true,
- },
- '/identicon': {
- target: httpUrl,
- rewrite(path) {
- return path.replace('@localhost:5173', '');
- },
- },
- '/url': httpUrl,
- '/proxy': httpUrl,
- '/_info_card_': httpUrl,
- '/bios': httpUrl,
- '/cli': httpUrl,
- '/inbox': httpUrl,
- '/emoji/': httpUrl,
- '/notes': {
- target: httpUrl,
- bypass: varyHandler,
- },
- '/users': {
- target: httpUrl,
- bypass: varyHandler,
- },
- '/.well-known': {
- target: httpUrl,
- },
- },
- },
- build: {
- ...defaultConfig.build,
- rollupOptions: {
- ...defaultConfig.build?.rollupOptions,
- input: 'index.html',
- },
- },
-
- define: {
- ...defaultConfig.define,
- _LANGS_FULL_: JSON.stringify(Object.entries(locales)),
- },
-};
-
-export default defineConfig(({ command, mode }) => devConfig);
-
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 2ba63c010f..5ff07d05c9 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -3,6 +3,7 @@ import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue';
import { type UserConfig, defineConfig } from 'vite';
import { localesVersion } from '../../locales/version.js';
+
import locales from '../../locales/index.js';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
@@ -77,6 +78,12 @@ export function getConfig(): UserConfig {
server: {
port: 5173,
+ hmr: {
+ // バックエンド経由での起動時、Viteは5173経由でアセットを参照していると思い込んでいるが実際は3000から配信される
+ // そのため、バックエンドのWSサーバーにHMRのWSリクエストが吸収されてしまい、正しくHMRが機能しない
+ // クライアント側のWSポートをViteサーバーのポートに強制させることで、正しくHMRが機能するようになる
+ clientPort: 5173,
+ },
headers: { // なんか効かない
'X-Frame-Options': 'DENY',
},
diff --git a/packages/frontend/vite.replaceIcons.ts b/packages/frontend/vite.replaceIcons.ts
index 9aa1e0c7b4..7c8b5678d6 100644
--- a/packages/frontend/vite.replaceIcons.ts
+++ b/packages/frontend/vite.replaceIcons.ts
@@ -227,6 +227,7 @@ export function pluginReplaceIcons() {
'ti ti-edit': 'ph-pencil-simple-line ph-bold ph-lg',
'ti ti-equal-double': 'ph-equals ph-bold ph-lg',
'ti ti-equal-not': 'ph-prohibit ph-bold ph-lg',
+ 'ti ti-eraser': 'ph-eraser ph-bold ph-lg',
'ti ti-exclamation-circle': 'ph-warning-circle ph-bold ph-lg',
'ti ti-external-link': 'ph-arrow-square-out ph-bold ph-lg',
'ti ti-eye': 'ph-eye ph-bold ph-lg',
@@ -256,6 +257,7 @@ export function pluginReplaceIcons() {
'ti ti-help-circle': 'ph-question ph-bold ph-lg',
'ti ti-home': 'ph-house ph-bold ph-lg',
'ti ti-hourglass-empty': 'ph-hourglass ph-bold ph-lg',
+ 'ti ti-icons': 'ph-squares-four ph-bold ph-lg',
'ti ti-id': 'ph-identification-card ph-bold ph-lg',
'ti ti-info-circle': 'ph-info ph-bold ph-lg',
'ti ti-json': 'ph-brackets-curly ph-bold ph-lg',
@@ -266,7 +268,8 @@ export function pluginReplaceIcons() {
'ti ti-link': 'ph-link ph-bold ph-lg',
'ti ti-link-off': 'ph-link-break ph-bold ph-lg',
'ti ti-list': 'ph-list ph-bold ph-lg',
- 'ti ti-list-search': 'ph-list ph-bold ph-lg-search',
+ 'ti ti-list-numbers': 'ph-list-numbers ph-bold ph-lg',
+ 'ti ti-list-search': 'ph-list ph-bold ph-lg',
'ti ti-lock': 'ph-lock ph-bold ph-lg',
'ti ti-lock-open': 'ph-lock-open ph-bold ph-lg',
'ti ti-lock-star': 'ph-shield-star ph-bold ph-lg',
@@ -293,6 +296,7 @@ export function pluginReplaceIcons() {
'ti ti-music': 'ph-music-notes ph-bold ph-lg',
'ti ti-news': 'ph-newspaper ph-bold ph-lg',
'ti ti-note': 'ph-note ph-bold ph-lg',
+ 'ti ti-notes': 'ph-notepad ph-bold ph-lg',
'ti ti-notebook': 'ph-notebook ph-bold ph-lg',
'ti ti-package': 'ph-package ph-bold ph-lg',
'ti ti-paint': 'ph-paint-roller ph-bold ph-lg',
@@ -365,6 +369,7 @@ export function pluginReplaceIcons() {
'ti ti-user-exclamation': 'ph-warning-circle ph-bold ph-lg',
'ti ti-user-off': 'ph-user-minus ph-bold ph-lg',
'ti ti-user-plus': 'ph-user-plus ph-bold ph-lg',
+ 'ti ti-user-question': 'ph-user-circle-dashed ph-bold ph-lg',
'ti ti-user-search': 'ph-user-circle ph-bold ph-lg',
'ti ti-user-shield': 'ph-newspaper-clipping ph-bold ph-lg',
'ti ti-user-star': 'ph-user-focus ph-bold ph-lg',
diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json
index c65797b78c..1472b8ee3d 100644
--- a/packages/megalodon/package.json
+++ b/packages/megalodon/package.json
@@ -9,9 +9,9 @@
"doc": "typedoc --out ../docs ./src",
"test": "cross-env NODE_ENV=test jest -u --maxWorkers=3"
},
- "engines": {
- "node": ">=15.0.0"
- },
+ "engines": {
+ "node": "^22.0.0"
+ },
"repository": {
"type": "git",
"url": "git+https://github.com/h3poteto/megalodon.git"
diff --git a/packages/megalodon/src/entities/relationship.ts b/packages/megalodon/src/entities/relationship.ts
index 283a1158c6..eb8daf9c7b 100644
--- a/packages/megalodon/src/entities/relationship.ts
+++ b/packages/megalodon/src/entities/relationship.ts
@@ -8,6 +8,7 @@ namespace Entity {
muting: boolean
muting_notifications: boolean
requested: boolean
+ requested_by?: boolean
domain_blocking: boolean
showing_reblogs: boolean
endorsed: boolean
diff --git a/packages/megalodon/src/index.ts b/packages/megalodon/src/index.ts
index 0d2b70c295..070c397d2d 100644
--- a/packages/megalodon/src/index.ts
+++ b/packages/megalodon/src/index.ts
@@ -27,7 +27,10 @@ export {
Pleroma,
Misskey,
Entity,
- Converter
+ Converter,
+ generator,
}
+export const megalodon = generator;
+
export default generator
diff --git a/packages/megalodon/src/mastodon/entities/relationship.ts b/packages/megalodon/src/mastodon/entities/relationship.ts
index 8e02df5769..f868a64063 100644
--- a/packages/megalodon/src/mastodon/entities/relationship.ts
+++ b/packages/megalodon/src/mastodon/entities/relationship.ts
@@ -8,6 +8,7 @@ namespace MastodonEntity {
muting: boolean
muting_notifications: boolean
requested: boolean
+ requested_by: boolean
domain_blocking: boolean
showing_reblogs: boolean
endorsed: boolean
diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts
index 7d68d4eddf..7e493d8fb7 100644
--- a/packages/megalodon/src/misskey.ts
+++ b/packages/megalodon/src/misskey.ts
@@ -604,7 +604,7 @@ export default class Misskey implements MegalodonInterface {
/**
* POST /api/users/relation
*
- * @param id Array of account ID, for example `['1sdfag', 'ds12aa']`.
+ * @param ids Array of account ID, for example `['1sdfag', 'ds12aa']`.
*/
public async getRelationships(ids: Array<string>): Promise<Response<Array<Entity.Relationship>>> {
return Promise.all(ids.map(id => this.getRelationship(id))).then(results => ({
@@ -2232,7 +2232,7 @@ export default class Misskey implements MegalodonInterface {
hashtags: [],
},
}));
-
+
if (result.status !== 200) {
result.status = 200;
result.statusText = "OK";
diff --git a/packages/megalodon/src/misskey/api_client.ts b/packages/megalodon/src/misskey/api_client.ts
index 8996b802c8..a4352613eb 100644
--- a/packages/megalodon/src/misskey/api_client.ts
+++ b/packages/megalodon/src/misskey/api_client.ts
@@ -227,13 +227,14 @@ namespace MisskeyAPI {
blocking: r.isBlocking,
blocked_by: r.isBlocked,
muting: r.isMuted,
- muting_notifications: false,
+ muting_notifications: r.isMuted,
requested: r.hasPendingFollowRequestFromYou,
- domain_blocking: false,
- showing_reblogs: true,
+ requested_by: r.hasPendingFollowRequestToYou,
+ domain_blocking: r.isInstanceMuted ?? false,
+ showing_reblogs: !r.isRenoteMuted,
endorsed: false,
- notifying: false,
- note: null
+ notifying: !r.isMuted,
+ note: r.memo ?? '',
}
}
diff --git a/packages/megalodon/src/misskey/entities/relation.ts b/packages/megalodon/src/misskey/entities/relation.ts
index 07653b4865..a43dfbab4e 100644
--- a/packages/megalodon/src/misskey/entities/relation.ts
+++ b/packages/megalodon/src/misskey/entities/relation.ts
@@ -8,5 +8,8 @@ namespace MisskeyEntity {
isBlocking: boolean
isBlocked: boolean
isMuted: boolean
+ isRenoteMuted: boolean
+ isInstanceMuted?: boolean
+ memo?: string | null
}
}
diff --git a/packages/megalodon/test/integration/detector.spec.ts b/packages/megalodon/test/integration/detector.spec.ts
deleted file mode 100644
index a7667d1c57..0000000000
--- a/packages/megalodon/test/integration/detector.spec.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { detector } from '../../src/index'
-
-describe('detector', () => {
- describe('mastodon', () => {
- const url = 'https://mastodon.social'
- it('should be mastodon', async () => {
- const mastodon = await detector(url)
- expect(mastodon).toEqual('mastodon')
- })
- })
-
- describe('pleroma', () => {
- const url = 'https://pleroma.io'
- it('should be pleroma', async () => {
- const pleroma = await detector(url)
- expect(pleroma).toEqual('pleroma')
- })
- })
-
- describe('misskey', () => {
- const url = 'https://misskey.io'
- it('should be misskey', async () => {
- const misskey = await detector(url)
- expect(misskey).toEqual('misskey')
- })
- })
-
- describe('fedibird', () => {
- const url = 'https://fedibird.com'
- it('should be mastodon', async () => {
- const fedibird = await detector(url)
- expect(fedibird).toEqual('mastodon')
- }, 20000)
- })
-
- describe('friendica', () => {
- const url = 'https://squeet.me'
- it('should be friendica', async () => {
- const friendica = await detector(url)
- expect(friendica).toEqual('friendica')
- })
- })
-
- describe('akkoma', () => {
- const url = 'https://pleroma.noellabo.jp'
- it('should be akkoma', async () => {
- const akkoma = await detector(url)
- expect(akkoma).toEqual('pleroma')
- })
- })
-
- // This domain no longer resolves, and resolution failures apparently crash jest
- // describe('wildebeest', () => {
- // const url = 'https://wildebeest.mirror-kt.dev'
- // it('should be mastodon', async () => {
- // const wildebeest = await detector(url)
- // expect(wildebeest).toEqual('mastodon')
- // })
- // })
-
- describe('unknown', () => {
- const url = 'https://google.com'
- it('should be null', async () => {
- const unknown = detector(url)
- await expect(unknown).rejects.toThrow()
- })
- })
-})
diff --git a/packages/megalodon/tsconfig.json b/packages/megalodon/tsconfig.json
index 6327d7c31d..2b90738318 100644
--- a/packages/megalodon/tsconfig.json
+++ b/packages/megalodon/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
/* Basic Options */
- "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
+ "target": "ES2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
- "lib": ["es2021", "dom"], /* Specify library files to be included in the compilation. */
+ "lib": ["ES2022", "dom"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
diff --git a/packages/misskey-bubble-game/build.js b/packages/misskey-bubble-game/build.js
index a80b71646f..5d534cc6fd 100644
--- a/packages/misskey-bubble-game/build.js
+++ b/packages/misskey-bubble-game/build.js
@@ -23,10 +23,14 @@ const options = {
sourcemap: 'linked',
};
+const args = process.argv.slice(2).map(arg => arg.toLowerCase());
+
// built配下をすべて削除する
-fs.rmSync('./built', { recursive: true, force: true });
+if (!args.includes('--no-clean')) {
+ fs.rmSync('./built', { recursive: true, force: true });
+}
-if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
+if (args.includes('--watch')) {
await watchSrc();
} else {
await buildSrc();
diff --git a/packages/misskey-js/LICENSE b/packages/misskey-js/LICENSE
index 63762b85d8..16352625db 100644
--- a/packages/misskey-js/LICENSE
+++ b/packages/misskey-js/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2021-2024 syuilo and other contributors
+Copyright (c) 2021-2025 syuilo and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/misskey-js/api-extractor.json b/packages/misskey-js/api-extractor.json
index a95281a6d5..627a245a49 100644
--- a/packages/misskey-js/api-extractor.json
+++ b/packages/misskey-js/api-extractor.json
@@ -62,6 +62,8 @@
*/
"bundledPackages": [],
+ "newlineKind": "lf",
+
/**
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
*/
diff --git a/packages/misskey-js/build.js b/packages/misskey-js/build.js
index a80b71646f..b794592815 100644
--- a/packages/misskey-js/build.js
+++ b/packages/misskey-js/build.js
@@ -24,9 +24,14 @@ const options = {
};
// built配下をすべて削除する
-fs.rmSync('./built', { recursive: true, force: true });
+const args = process.argv.slice(2).map(arg => arg.toLowerCase());
-if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
+// built配下をすべて削除する
+if (!args.includes('--no-clean')) {
+ fs.rmSync('./built', { recursive: true, force: true });
+}
+
+if (args.includes('--watch')) {
await watchSrc();
} else {
await buildSrc();
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 0269cc2c86..e5d4bb6143 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -6,8 +6,9 @@
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import { EventEmitter } from 'eventemitter3';
+import { Options } from 'reconnecting-websocket';
import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
-import _ReconnectingWebsocket from 'reconnecting-websocket';
+import _ReconnectingWebSocket from 'reconnecting-websocket';
// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
//
@@ -140,6 +141,15 @@ type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations
type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json'];
// @public (undocumented)
+type AdminCaptchaCurrentResponse = operations['admin___captcha___current']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminCaptchaSaveRequest = operations['admin___captcha___save']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminCwUserRequest = operations['admin___cw-user']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json'];
// @public (undocumented)
@@ -272,6 +282,9 @@ type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBo
type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
// @public (undocumented)
+type AdminRejectQuotesRequest = operations['admin___reject-quotes']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
// @public (undocumented)
@@ -1145,6 +1158,9 @@ type EmojiDeleted = {
type EmojiDetailed = components['schemas']['EmojiDetailed'];
// @public (undocumented)
+type EmojiDetailedAdmin = components['schemas']['EmojiDetailedAdmin'];
+
+// @public (undocumented)
type EmojiRequest = operations['emoji']['requestBody']['content']['application/json'];
// @public (undocumented)
@@ -1260,18 +1276,17 @@ declare namespace entities {
PartialRolePolicyOverride,
EmptyRequest,
EmptyResponse,
- AdminMetaResponse,
- AdminAbuseUserReportsRequest,
- AdminAbuseUserReportsResponse,
+ AdminAbuseReportNotificationRecipientCreateRequest,
+ AdminAbuseReportNotificationRecipientCreateResponse,
+ AdminAbuseReportNotificationRecipientDeleteRequest,
AdminAbuseReportNotificationRecipientListRequest,
AdminAbuseReportNotificationRecipientListResponse,
AdminAbuseReportNotificationRecipientShowRequest,
AdminAbuseReportNotificationRecipientShowResponse,
- AdminAbuseReportNotificationRecipientCreateRequest,
- AdminAbuseReportNotificationRecipientCreateResponse,
AdminAbuseReportNotificationRecipientUpdateRequest,
AdminAbuseReportNotificationRecipientUpdateResponse,
- AdminAbuseReportNotificationRecipientDeleteRequest,
+ AdminAbuseUserReportsRequest,
+ AdminAbuseUserReportsResponse,
AdminAccountsCreateRequest,
AdminAccountsCreateResponse,
AdminAccountsDeleteRequest,
@@ -1289,31 +1304,35 @@ declare namespace entities {
AdminAnnouncementsListRequest,
AdminAnnouncementsListResponse,
AdminAnnouncementsUpdateRequest,
+ AdminApproveUserRequest,
AdminAvatarDecorationsCreateRequest,
AdminAvatarDecorationsCreateResponse,
AdminAvatarDecorationsDeleteRequest,
AdminAvatarDecorationsListRequest,
AdminAvatarDecorationsListResponse,
AdminAvatarDecorationsUpdateRequest,
+ AdminCaptchaCurrentResponse,
+ AdminCaptchaSaveRequest,
+ AdminCwUserRequest,
+ AdminDeclineUserRequest,
+ AdminDeleteAccountRequest,
AdminDeleteAllFilesOfAUserRequest,
- AdminUnsetUserAvatarRequest,
- AdminUnsetUserBannerRequest,
AdminDriveFilesRequest,
AdminDriveFilesResponse,
AdminDriveShowFileRequest,
AdminDriveShowFileResponse,
- AdminEmojiAddAliasesBulkRequest,
AdminEmojiAddRequest,
AdminEmojiAddResponse,
+ AdminEmojiAddAliasesBulkRequest,
AdminEmojiCopyRequest,
AdminEmojiCopyResponse,
- AdminEmojiDeleteBulkRequest,
AdminEmojiDeleteRequest,
+ AdminEmojiDeleteBulkRequest,
AdminEmojiImportZipRequest,
- AdminEmojiListRemoteRequest,
- AdminEmojiListRemoteResponse,
AdminEmojiListRequest,
AdminEmojiListResponse,
+ AdminEmojiListRemoteRequest,
+ AdminEmojiListRemoteResponse,
AdminEmojiRemoveAliasesBulkRequest,
AdminEmojiSetAliasesBulkRequest,
AdminEmojiSetCategoryBulkRequest,
@@ -1323,6 +1342,7 @@ declare namespace entities {
AdminFederationRefreshRemoteInstanceMetadataRequest,
AdminFederationRemoveAllFollowingRequest,
AdminFederationUpdateInstanceRequest,
+ AdminForwardAbuseUserReportRequest,
AdminGetIndexStatsResponse,
AdminGetTableStatsResponse,
AdminGetUserIpsRequest,
@@ -1331,11 +1351,14 @@ declare namespace entities {
AdminInviteCreateResponse,
AdminInviteListRequest,
AdminInviteListResponse,
+ AdminMetaResponse,
+ AdminNsfwUserRequest,
AdminPromoCreateRequest,
AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse,
AdminQueuePromoteRequest,
AdminQueueStatsResponse,
+ AdminRejectQuotesRequest,
AdminRelaysAddRequest,
AdminRelaysAddResponse,
AdminRelaysListResponse,
@@ -1343,39 +1366,28 @@ declare namespace entities {
AdminResetPasswordRequest,
AdminResetPasswordResponse,
AdminResolveAbuseUserReportRequest,
- AdminForwardAbuseUserReportRequest,
- AdminUpdateAbuseUserReportRequest,
- AdminSendEmailRequest,
- AdminServerInfoResponse,
- AdminShowModerationLogsRequest,
- AdminShowModerationLogsResponse,
- AdminShowUserRequest,
- AdminShowUserResponse,
- AdminShowUsersRequest,
- AdminShowUsersResponse,
- AdminNsfwUserRequest,
- AdminUnnsfwUserRequest,
- AdminSilenceUserRequest,
- AdminUnsilenceUserRequest,
- AdminSuspendUserRequest,
- AdminApproveUserRequest,
- AdminDeclineUserRequest,
- AdminUnsuspendUserRequest,
- AdminUpdateMetaRequest,
- AdminDeleteAccountRequest,
- AdminUpdateUserNoteRequest,
+ AdminRolesAssignRequest,
AdminRolesCreateRequest,
AdminRolesCreateResponse,
AdminRolesDeleteRequest,
AdminRolesListResponse,
AdminRolesShowRequest,
AdminRolesShowResponse,
- AdminRolesUpdateRequest,
- AdminRolesAssignRequest,
AdminRolesUnassignRequest,
+ AdminRolesUpdateRequest,
AdminRolesUpdateDefaultPoliciesRequest,
AdminRolesUsersRequest,
AdminRolesUsersResponse,
+ AdminSendEmailRequest,
+ AdminServerInfoResponse,
+ AdminShowModerationLogsRequest,
+ AdminShowModerationLogsResponse,
+ AdminShowUserRequest,
+ AdminShowUserResponse,
+ AdminShowUsersRequest,
+ AdminShowUsersResponse,
+ AdminSilenceUserRequest,
+ AdminSuspendUserRequest,
AdminSystemWebhookCreateRequest,
AdminSystemWebhookCreateResponse,
AdminSystemWebhookDeleteRequest,
@@ -1383,9 +1395,17 @@ declare namespace entities {
AdminSystemWebhookListResponse,
AdminSystemWebhookShowRequest,
AdminSystemWebhookShowResponse,
+ AdminSystemWebhookTestRequest,
AdminSystemWebhookUpdateRequest,
AdminSystemWebhookUpdateResponse,
- AdminSystemWebhookTestRequest,
+ AdminUnnsfwUserRequest,
+ AdminUnsetUserAvatarRequest,
+ AdminUnsetUserBannerRequest,
+ AdminUnsilenceUserRequest,
+ AdminUnsuspendUserRequest,
+ AdminUpdateAbuseUserReportRequest,
+ AdminUpdateMetaRequest,
+ AdminUpdateUserNoteRequest,
AnnouncementsRequest,
AnnouncementsResponse,
AnnouncementsShowRequest,
@@ -1421,26 +1441,29 @@ declare namespace entities {
BlockingDeleteResponse,
BlockingListRequest,
BlockingListResponse,
+ BubbleGameRankingRequest,
+ BubbleGameRankingResponse,
+ BubbleGameRegisterRequest,
ChannelsCreateRequest,
ChannelsCreateResponse,
+ ChannelsFavoriteRequest,
ChannelsFeaturedResponse,
ChannelsFollowRequest,
ChannelsFollowedRequest,
ChannelsFollowedResponse,
+ ChannelsMyFavoritesResponse,
ChannelsOwnedRequest,
ChannelsOwnedResponse,
+ ChannelsSearchRequest,
+ ChannelsSearchResponse,
ChannelsShowRequest,
ChannelsShowResponse,
ChannelsTimelineRequest,
ChannelsTimelineResponse,
+ ChannelsUnfavoriteRequest,
ChannelsUnfollowRequest,
ChannelsUpdateRequest,
ChannelsUpdateResponse,
- ChannelsFavoriteRequest,
- ChannelsUnfavoriteRequest,
- ChannelsMyFavoritesResponse,
- ChannelsSearchRequest,
- ChannelsSearchResponse,
ChartsActiveUsersRequest,
ChartsActiveUsersResponse,
ChartsApRequestRequest,
@@ -1466,20 +1489,20 @@ declare namespace entities {
ChartsUsersRequest,
ChartsUsersResponse,
ClipsAddNoteRequest,
- ClipsRemoveNoteRequest,
ClipsCreateRequest,
ClipsCreateResponse,
ClipsDeleteRequest,
+ ClipsFavoriteRequest,
ClipsListResponse,
+ ClipsMyFavoritesResponse,
ClipsNotesRequest,
ClipsNotesResponse,
+ ClipsRemoveNoteRequest,
ClipsShowRequest,
ClipsShowResponse,
+ ClipsUnfavoriteRequest,
ClipsUpdateRequest,
ClipsUpdateResponse,
- ClipsFavoriteRequest,
- ClipsUnfavoriteRequest,
- ClipsMyFavoritesResponse,
DriveResponse,
DriveFilesRequest,
DriveFilesResponse,
@@ -1490,10 +1513,10 @@ declare namespace entities {
DriveFilesCreateRequest,
DriveFilesCreateResponse,
DriveFilesDeleteRequest,
- DriveFilesFindByHashRequest,
- DriveFilesFindByHashResponse,
DriveFilesFindRequest,
DriveFilesFindResponse,
+ DriveFilesFindByHashRequest,
+ DriveFilesFindByHashResponse,
DriveFilesShowRequest,
DriveFilesShowResponse,
DriveFilesUpdateRequest,
@@ -1514,6 +1537,9 @@ declare namespace entities {
DriveStreamResponse,
EmailAddressAvailableRequest,
EmailAddressAvailableResponse,
+ EmojiRequest,
+ EmojiResponse,
+ EmojisResponse,
EndpointRequest,
EndpointResponse,
EndpointsResponse,
@@ -1525,18 +1551,33 @@ declare namespace entities {
FederationInstancesResponse,
FederationShowInstanceRequest,
FederationShowInstanceResponse,
+ FederationStatsRequest,
+ FederationStatsResponse,
FederationUpdateRemoteUserRequest,
FederationUsersRequest,
FederationUsersResponse,
- FederationStatsRequest,
- FederationStatsResponse,
+ FetchExternalResourcesRequest,
+ FetchExternalResourcesResponse,
+ FetchRssRequest,
+ FetchRssResponse,
+ FlashCreateRequest,
+ FlashCreateResponse,
+ FlashDeleteRequest,
+ FlashFeaturedRequest,
+ FlashFeaturedResponse,
+ FlashLikeRequest,
+ FlashMyRequest,
+ FlashMyResponse,
+ FlashMyLikesRequest,
+ FlashMyLikesResponse,
+ FlashShowRequest,
+ FlashShowResponse,
+ FlashUnlikeRequest,
+ FlashUpdateRequest,
FollowingCreateRequest,
FollowingCreateResponse,
FollowingDeleteRequest,
FollowingDeleteResponse,
- FollowingUpdateRequest,
- FollowingUpdateResponse,
- FollowingUpdateAllRequest,
FollowingInvalidateRequest,
FollowingInvalidateResponse,
FollowingRequestsAcceptRequest,
@@ -1544,9 +1585,12 @@ declare namespace entities {
FollowingRequestsCancelResponse,
FollowingRequestsListRequest,
FollowingRequestsListResponse,
+ FollowingRequestsRejectRequest,
FollowingRequestsSentRequest,
FollowingRequestsSentResponse,
- FollowingRequestsRejectRequest,
+ FollowingUpdateRequest,
+ FollowingUpdateResponse,
+ FollowingUpdateAllRequest,
GalleryFeaturedRequest,
GalleryFeaturedResponse,
GalleryPopularResponse,
@@ -1561,8 +1605,8 @@ declare namespace entities {
GalleryPostsUnlikeRequest,
GalleryPostsUpdateRequest,
GalleryPostsUpdateResponse,
- GetOnlineUsersCountResponse,
GetAvatarDecorationsResponse,
+ GetOnlineUsersCountResponse,
HashtagsListRequest,
HashtagsListResponse,
HashtagsSearchRequest,
@@ -1578,19 +1622,19 @@ declare namespace entities {
I2faKeyDoneRequest,
I2faKeyDoneResponse,
I2faPasswordLessRequest,
- I2faRegisterKeyRequest,
- I2faRegisterKeyResponse,
I2faRegisterRequest,
I2faRegisterResponse,
- I2faUpdateKeyRequest,
+ I2faRegisterKeyRequest,
+ I2faRegisterKeyResponse,
I2faRemoveKeyRequest,
I2faUnregisterRequest,
+ I2faUpdateKeyRequest,
IAppsRequest,
IAppsResponse,
IAuthorizedAppsRequest,
IAuthorizedAppsResponse,
- IClaimAchievementRequest,
IChangePasswordRequest,
+ IClaimAchievementRequest,
IDeleteAccountRequest,
IExportFollowingRequest,
IFavoritesRequest,
@@ -1599,12 +1643,14 @@ declare namespace entities {
IGalleryLikesResponse,
IGalleryPostsRequest,
IGalleryPostsResponse,
+ IImportAntennasRequest,
IImportBlockingRequest,
IImportFollowingRequest,
- IImportNotesRequest,
IImportMutingRequest,
+ IImportNotesRequest,
IImportUserListsRequest,
- IImportAntennasRequest,
+ IMoveRequest,
+ IMoveResponse,
INotificationsRequest,
INotificationsResponse,
INotificationsGroupedRequest,
@@ -1617,17 +1663,17 @@ declare namespace entities {
IPinResponse,
IReadAnnouncementRequest,
IRegenerateTokenRequest,
+ IRegistryGetRequest,
+ IRegistryGetResponse,
IRegistryGetAllRequest,
IRegistryGetAllResponse,
- IRegistryGetUnsecureRequest,
IRegistryGetDetailRequest,
IRegistryGetDetailResponse,
- IRegistryGetRequest,
- IRegistryGetResponse,
- IRegistryKeysWithTypeRequest,
- IRegistryKeysWithTypeResponse,
+ IRegistryGetUnsecureRequest,
IRegistryKeysRequest,
IRegistryKeysResponse,
+ IRegistryKeysWithTypeRequest,
+ IRegistryKeysWithTypeResponse,
IRegistryRemoveRequest,
IRegistryScopesWithDomainResponse,
IRegistrySetRequest,
@@ -1636,44 +1682,37 @@ declare namespace entities {
ISigninHistoryResponse,
IUnpinRequest,
IUnpinResponse,
- IUpdateEmailRequest,
- IUpdateEmailResponse,
IUpdateRequest,
IUpdateResponse,
- IMoveRequest,
- IMoveResponse,
+ IUpdateEmailRequest,
+ IUpdateEmailResponse,
IWebhooksCreateRequest,
IWebhooksCreateResponse,
+ IWebhooksDeleteRequest,
IWebhooksListResponse,
IWebhooksShowRequest,
IWebhooksShowResponse,
- IWebhooksUpdateRequest,
- IWebhooksDeleteRequest,
IWebhooksTestRequest,
+ IWebhooksUpdateRequest,
InviteCreateResponse,
InviteDeleteRequest,
+ InviteLimitResponse,
InviteListRequest,
InviteListResponse,
- InviteLimitResponse,
MetaRequest,
MetaResponse,
- EmojisResponse,
- EmojiRequest,
- EmojiResponse,
MiauthGenTokenRequest,
MiauthGenTokenResponse,
MuteCreateRequest,
MuteDeleteRequest,
MuteListRequest,
MuteListResponse,
- RenoteMuteCreateRequest,
- RenoteMuteDeleteRequest,
- RenoteMuteListRequest,
- RenoteMuteListResponse,
MyAppsRequest,
MyAppsResponse,
NotesRequest,
NotesResponse,
+ NotesBubbleTimelineRequest,
+ NotesBubbleTimelineResponse,
NotesChildrenRequest,
NotesChildrenResponse,
NotesClipsRequest,
@@ -1683,6 +1722,8 @@ declare namespace entities {
NotesCreateRequest,
NotesCreateResponse,
NotesDeleteRequest,
+ NotesEditRequest,
+ NotesEditResponse,
NotesFavoritesCreateRequest,
NotesFavoritesDeleteRequest,
NotesFeaturedRequest,
@@ -1691,23 +1732,21 @@ declare namespace entities {
NotesFollowingResponse,
NotesGlobalTimelineRequest,
NotesGlobalTimelineResponse,
- NotesBubbleTimelineRequest,
- NotesBubbleTimelineResponse,
NotesHybridTimelineRequest,
NotesHybridTimelineResponse,
+ NotesLikeRequest,
NotesLocalTimelineRequest,
NotesLocalTimelineResponse,
NotesMentionsRequest,
NotesMentionsResponse,
NotesPollsRecommendationRequest,
NotesPollsRecommendationResponse,
- NotesPollsVoteRequest,
NotesPollsRefreshRequest,
+ NotesPollsVoteRequest,
NotesReactionsRequest,
NotesReactionsResponse,
NotesReactionsCreateRequest,
NotesReactionsDeleteRequest,
- NotesLikeRequest,
NotesRenotesRequest,
NotesRenotesResponse,
NotesRepliesRequest,
@@ -1716,10 +1755,10 @@ declare namespace entities {
NotesScheduleDeleteRequest,
NotesScheduleListRequest,
NotesScheduleListResponse,
- NotesSearchByTagRequest,
- NotesSearchByTagResponse,
NotesSearchRequest,
NotesSearchResponse,
+ NotesSearchByTagRequest,
+ NotesSearchByTagResponse,
NotesShowRequest,
NotesShowResponse,
NotesStateRequest,
@@ -1733,8 +1772,6 @@ declare namespace entities {
NotesUnrenoteRequest,
NotesUserListTimelineRequest,
NotesUserListTimelineResponse,
- NotesEditRequest,
- NotesEditResponse,
NotesVersionsRequest,
NotesVersionsResponse,
NotificationsCreateRequest,
@@ -1748,49 +1785,58 @@ declare namespace entities {
PagesShowResponse,
PagesUnlikeRequest,
PagesUpdateRequest,
- FlashCreateRequest,
- FlashCreateResponse,
- FlashDeleteRequest,
- FlashFeaturedRequest,
- FlashFeaturedResponse,
- FlashLikeRequest,
- FlashShowRequest,
- FlashShowResponse,
- FlashUnlikeRequest,
- FlashUpdateRequest,
- FlashMyRequest,
- FlashMyResponse,
- FlashMyLikesRequest,
- FlashMyLikesResponse,
PingResponse,
PinnedUsersResponse,
PromoReadRequest,
+ RenoteMuteCreateRequest,
+ RenoteMuteDeleteRequest,
+ RenoteMuteListRequest,
+ RenoteMuteListResponse,
+ RequestResetPasswordRequest,
+ ResetPasswordRequest,
+ RetentionResponse,
+ ReversiCancelMatchRequest,
+ ReversiGamesRequest,
+ ReversiGamesResponse,
+ ReversiInvitationsResponse,
+ ReversiMatchRequest,
+ ReversiMatchResponse,
+ ReversiShowGameRequest,
+ ReversiShowGameResponse,
+ ReversiSurrenderRequest,
+ ReversiVerifyRequest,
+ ReversiVerifyResponse,
RolesListResponse,
+ RolesNotesRequest,
+ RolesNotesResponse,
RolesShowRequest,
RolesShowResponse,
RolesUsersRequest,
RolesUsersResponse,
- RolesNotesRequest,
- RolesNotesResponse,
- RequestResetPasswordRequest,
- ResetPasswordRequest,
ServerInfoResponse,
+ SponsorsRequest,
StatsResponse,
+ SwRegisterRequest,
+ SwRegisterResponse,
SwShowRegistrationRequest,
SwShowRegistrationResponse,
+ SwUnregisterRequest,
SwUpdateRegistrationRequest,
SwUpdateRegistrationResponse,
- SwRegisterRequest,
- SwRegisterResponse,
- SwUnregisterRequest,
TestRequest,
TestResponse,
UsernameAvailableRequest,
UsernameAvailableResponse,
UsersRequest,
UsersResponse,
+ UsersAchievementsRequest,
+ UsersAchievementsResponse,
UsersClipsRequest,
UsersClipsResponse,
+ UsersFeaturedNotesRequest,
+ UsersFeaturedNotesResponse,
+ UsersFlashsRequest,
+ UsersFlashsResponse,
UsersFollowersRequest,
UsersFollowersResponse,
UsersFollowingRequest,
@@ -1799,32 +1845,28 @@ declare namespace entities {
UsersGalleryPostsResponse,
UsersGetFrequentlyRepliedUsersRequest,
UsersGetFrequentlyRepliedUsersResponse,
- UsersFeaturedNotesRequest,
- UsersFeaturedNotesResponse,
UsersListsCreateRequest,
UsersListsCreateResponse,
+ UsersListsCreateFromPublicRequest,
+ UsersListsCreateFromPublicResponse,
UsersListsDeleteRequest,
+ UsersListsFavoriteRequest,
+ UsersListsGetMembershipsRequest,
+ UsersListsGetMembershipsResponse,
UsersListsListRequest,
UsersListsListResponse,
UsersListsPullRequest,
UsersListsPushRequest,
UsersListsShowRequest,
UsersListsShowResponse,
- UsersListsFavoriteRequest,
UsersListsUnfavoriteRequest,
UsersListsUpdateRequest,
UsersListsUpdateResponse,
- UsersListsCreateFromPublicRequest,
- UsersListsCreateFromPublicResponse,
UsersListsUpdateMembershipRequest,
- UsersListsGetMembershipsRequest,
- UsersListsGetMembershipsResponse,
UsersNotesRequest,
UsersNotesResponse,
UsersPagesRequest,
UsersPagesResponse,
- UsersFlashsRequest,
- UsersFlashsResponse,
UsersReactionsRequest,
UsersReactionsResponse,
UsersRecommendationRequest,
@@ -1832,35 +1874,15 @@ declare namespace entities {
UsersRelationRequest,
UsersRelationResponse,
UsersReportAbuseRequest,
- UsersSearchByUsernameAndHostRequest,
- UsersSearchByUsernameAndHostResponse,
UsersSearchRequest,
UsersSearchResponse,
+ UsersSearchByUsernameAndHostRequest,
+ UsersSearchByUsernameAndHostResponse,
UsersShowRequest,
UsersShowResponse,
- UsersAchievementsRequest,
- UsersAchievementsResponse,
UsersUpdateMemoRequest,
- FetchRssRequest,
- FetchRssResponse,
- FetchExternalResourcesRequest,
- FetchExternalResourcesResponse,
- RetentionResponse,
- SponsorsRequest,
- BubbleGameRegisterRequest,
- BubbleGameRankingRequest,
- BubbleGameRankingResponse,
- ReversiCancelMatchRequest,
- ReversiGamesRequest,
- ReversiGamesResponse,
- ReversiMatchRequest,
- ReversiMatchResponse,
- ReversiInvitationsResponse,
- ReversiShowGameRequest,
- ReversiShowGameResponse,
- ReversiSurrenderRequest,
- ReversiVerifyRequest,
- ReversiVerifyResponse,
+ V2AdminEmojiListRequest,
+ V2AdminEmojiListResponse,
Error_2 as Error,
UserLite,
UserDetailedNotMeOnly,
@@ -1895,6 +1917,7 @@ declare namespace entities {
GalleryPost,
EmojiSimple,
EmojiDetailed,
+ EmojiDetailedAdmin,
Flash,
Signin,
RoleCondFormulaLogics,
@@ -2538,6 +2561,18 @@ type ModerationLog = {
type: 'unsuspend';
info: ModerationLogPayloads['unsuspend'];
} | {
+ type: 'acceptQuotesUser';
+ info: ModerationLogPayloads['acceptQuotesUser'];
+} | {
+ type: 'rejectQuotesUser';
+ info: ModerationLogPayloads['rejectQuotesUser'];
+} | {
+ type: 'acceptQuotesInstance';
+ info: ModerationLogPayloads['acceptQuotesInstance'];
+} | {
+ type: 'rejectQuotesInstance';
+ info: ModerationLogPayloads['rejectQuotesInstance'];
+} | {
type: 'updateUserNote';
info: ModerationLogPayloads['updateUserNote'];
} | {
@@ -2595,6 +2630,15 @@ type ModerationLog = {
type: 'deleteUserAnnouncement';
info: ModerationLogPayloads['deleteUserAnnouncement'];
} | {
+ type: 'setMandatoryCW';
+ info: ModerationLogPayloads['setMandatoryCW'];
+} | {
+ type: 'setRemoteInstanceNSFW';
+ info: ModerationLogPayloads['setRemoteInstanceNSFW'];
+} | {
+ type: 'unsetRemoteInstanceNSFW';
+ info: ModerationLogPayloads['unsetRemoteInstanceNSFW'];
+} | {
type: 'resetPassword';
info: ModerationLogPayloads['resetPassword'];
} | {
@@ -2604,6 +2648,12 @@ type ModerationLog = {
type: 'unsuspendRemoteInstance';
info: ModerationLogPayloads['unsuspendRemoteInstance'];
} | {
+ type: 'rejectRemoteInstanceReports';
+ info: ModerationLogPayloads['rejectRemoteInstanceReports'];
+} | {
+ type: 'acceptRemoteInstanceReports';
+ info: ModerationLogPayloads['acceptRemoteInstanceReports'];
+} | {
type: 'updateRemoteInstanceNote';
info: ModerationLogPayloads['updateRemoteInstanceNote'];
} | {
@@ -2678,10 +2728,55 @@ type ModerationLog = {
} | {
type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost'];
+} | {
+ type: 'clearUserFiles';
+ info: ModerationLogPayloads['clearUserFiles'];
+} | {
+ type: 'nsfwUser';
+ info: ModerationLogPayloads['nsfwUser'];
+} | {
+ type: 'unNsfwUser';
+ info: ModerationLogPayloads['unNsfwUser'];
+} | {
+ type: 'silenceUser';
+ info: ModerationLogPayloads['silenceUser'];
+} | {
+ type: 'unSilenceUser';
+ info: ModerationLogPayloads['unSilenceUser'];
+} | {
+ type: 'createAccount';
+ info: ModerationLogPayloads['createAccount'];
+} | {
+ type: 'clearRemoteFiles';
+ info: ModerationLogPayloads['clearRemoteFiles'];
+} | {
+ type: 'clearOwnerlessFiles';
+ info: ModerationLogPayloads['clearOwnerlessFiles'];
+} | {
+ type: 'updateCustomEmojis';
+ info: ModerationLogPayloads['updateCustomEmojis'];
+} | {
+ type: 'importCustomEmojis';
+ info: ModerationLogPayloads['importCustomEmojis'];
+} | {
+ type: 'clearInstanceFiles';
+ info: ModerationLogPayloads['clearInstanceFiles'];
+} | {
+ type: 'severFollowRelations';
+ info: ModerationLogPayloads['severFollowRelations'];
+} | {
+ type: 'createPromo';
+ info: ModerationLogPayloads['createPromo'];
+} | {
+ type: 'addRelay';
+ info: ModerationLogPayloads['addRelay'];
+} | {
+ type: 'removeRelay';
+ info: ModerationLogPayloads['removeRelay'];
});
// @public (undocumented)
-export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "approve", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
+export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "approve", "decline", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "setMandatoryCW", "setRemoteInstanceNSFW", "unsetRemoteInstanceNSFW", "suspendRemoteInstance", "unsuspendRemoteInstance", "rejectRemoteInstanceReports", "acceptRemoteInstanceReports", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@@ -2991,7 +3086,7 @@ type PartialRolePolicyOverride = Partial<{
}>;
// @public (undocumented)
-export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
+export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:cw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:reject-quotes", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
// @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
@@ -3264,7 +3359,8 @@ export class Stream extends EventEmitter<StreamEvents> implements IStream {
constructor(origin: string, user: {
token: string;
} | null, options?: {
- WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
+ WebSocket?: Options['WebSocket'];
+ binaryType?: ReconnectingWebSocket['binaryType'];
});
// (undocumented)
close(): void;
@@ -3524,9 +3620,16 @@ type UsersShowResponse = operations['users___show']['responses']['200']['content
// @public (undocumented)
type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json'];
+// @public (undocumented)
+type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['responses']['200']['content']['application/json'];
+
// Warnings were encountered during analysis:
//
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
+// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:236:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:246:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json
index f64150882d..9ed5b8470f 100644
--- a/packages/misskey-js/generator/package.json
+++ b/packages/misskey-js/generator/package.json
@@ -11,6 +11,7 @@
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
+ "eslint": "9.14.0",
"openapi-types": "12.1.3",
"openapi-typescript": "6.7.3",
"ts-case-convert": "2.1.0",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index f65d270a3d..1b4675b938 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2024.11.2",
+ "version": "2025.2.0-dev",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts
index ed1282957f..e663d712a7 100644
--- a/packages/misskey-js/src/api.ts
+++ b/packages/misskey-js/src/api.ts
@@ -44,7 +44,7 @@ export class APIClient {
credential?: APIClient['credential'];
fetch?: APIClient['fetch'] | null | undefined;
}) {
- this.origin = opts.origin;
+ this.origin = opts.origin.replace(/\/$/, '');
this.credential = opts.credential;
// ネイティブ関数をそのまま変数に代入して使おうとするとChromiumではIllegal invocationエラーが発生するため、
// 環境で実装されているfetchを使う場合は無名関数でラップして使用する
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index ccb513b7f9..2f3cd3b2ad 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -6,9 +6,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
*/
- request<E extends 'admin/meta', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/abuse-report/notification-recipient/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -17,9 +18,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
*/
- request<E extends 'admin/abuse-user-reports', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/abuse-report/notification-recipient/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -55,18 +57,6 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
*/
- request<E extends 'admin/abuse-report/notification-recipient/create', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
- */
request<E extends 'admin/abuse-report/notification-recipient/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
@@ -76,10 +66,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports*
*/
- request<E extends 'admin/abuse-report/notification-recipient/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/abuse-user-reports', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -209,6 +198,17 @@ declare module '../api.js' {
/**
* No description provided.
*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
+ */
+ request<E extends 'admin/approve-user', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
* **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations*
*/
request<E extends 'admin/avatar-decorations/create', P extends Endpoints[E]['req']>(
@@ -253,9 +253,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
*/
- request<E extends 'admin/delete-all-files-of-a-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/captcha/current', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -264,9 +264,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
*/
- request<E extends 'admin/unset-user-avatar', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/captcha/save', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -275,9 +275,42 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:cw-user*
*/
- request<E extends 'admin/unset-user-banner', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/cw-user', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
+ */
+ request<E extends 'admin/decline-user', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account*
+ */
+ request<E extends 'admin/delete-account', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user*
+ */
+ request<E extends 'admin/delete-all-files-of-a-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -332,7 +365,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- request<E extends 'admin/emoji/add-aliases-bulk', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/emoji/add', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -343,7 +376,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- request<E extends 'admin/emoji/add', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/emoji/add-aliases-bulk', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -365,7 +398,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- request<E extends 'admin/emoji/delete-bulk', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/emoji/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -376,7 +409,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- request<E extends 'admin/emoji/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/emoji/delete-bulk', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -399,7 +432,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- request<E extends 'admin/emoji/list-remote', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/emoji/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -410,7 +443,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- request<E extends 'admin/emoji/list', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/emoji/list-remote', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -518,6 +551,28 @@ declare module '../api.js' {
/**
* No description provided.
*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ request<E extends 'admin/forward-abuse-user-report', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
+ */
+ request<E extends 'admin/gen-vapid-keys', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
* **Credential required**: *Yes* / **Permission**: *read:admin:index-stats*
*/
request<E extends 'admin/get-index-stats', P extends Endpoints[E]['req']>(
@@ -573,9 +628,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:promo*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
*/
- request<E extends 'admin/promo/create', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/meta', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -584,9 +639,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:queue*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:nsfw-user*
*/
- request<E extends 'admin/queue/clear', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/nsfw-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -595,9 +650,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:queue*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:promo*
*/
- request<E extends 'admin/queue/deliver-delayed', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/promo/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -606,9 +661,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:queue*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:queue*
*/
- request<E extends 'admin/queue/inbox-delayed', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/queue/clear', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -617,9 +672,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:queue*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:queue*
*/
- request<E extends 'admin/queue/promote', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/queue/deliver-delayed', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -628,9 +683,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:queue*
*/
- request<E extends 'admin/queue/stats', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/queue/inbox-delayed', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -639,9 +694,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:relays*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:queue*
*/
- request<E extends 'admin/relays/add', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/queue/promote', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -650,9 +705,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:relays*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- request<E extends 'admin/relays/list', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/queue/stats', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -661,9 +716,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:relays*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:reject-quotes*
*/
- request<E extends 'admin/relays/remove', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/reject-quotes', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -672,9 +727,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:reset-password*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:relays*
*/
- request<E extends 'admin/reset-password', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/relays/add', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -683,9 +738,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:relays*
*/
- request<E extends 'admin/resolve-abuse-user-report', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/relays/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -694,9 +749,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:relays*
*/
- request<E extends 'admin/forward-abuse-user-report', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/relays/remove', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -705,9 +760,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:reset-password*
*/
- request<E extends 'admin/update-abuse-user-report', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/reset-password', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -716,9 +771,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:send-email*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
- request<E extends 'admin/send-email', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/resolve-abuse-user-report', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -727,9 +782,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:server-info*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- request<E extends 'admin/server-info', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/assign', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -738,9 +793,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- request<E extends 'admin/show-moderation-logs', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -749,9 +804,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- request<E extends 'admin/show-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -760,9 +815,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
*/
- request<E extends 'admin/show-users', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -771,9 +826,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:nsfw-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
*/
- request<E extends 'admin/nsfw-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/show', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -782,9 +837,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unnsfw-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- request<E extends 'admin/unnsfw-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/unassign', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -793,9 +848,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:silence-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- request<E extends 'admin/silence-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -804,9 +859,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unsilence-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- request<E extends 'admin/unsilence-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/update-default-policies', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -815,9 +870,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
+ * **Credential required**: *No* / **Permission**: *read:admin:roles*
*/
- request<E extends 'admin/suspend-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/roles/users', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -826,9 +881,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:send-email*
*/
- request<E extends 'admin/approve-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/send-email', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -837,9 +892,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:server-info*
*/
- request<E extends 'admin/decline-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/server-info', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -848,9 +903,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log*
*/
- request<E extends 'admin/unsuspend-user', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/show-moderation-logs', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -859,9 +914,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/
- request<E extends 'admin/update-meta', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/show-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -870,9 +925,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/
- request<E extends 'admin/delete-account', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/show-users', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -881,9 +936,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:user-note*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:silence-user*
*/
- request<E extends 'admin/update-user-note', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/silence-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -892,9 +947,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
*/
- request<E extends 'admin/roles/create', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/suspend-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -903,9 +958,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- request<E extends 'admin/roles/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/system-webhook/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -914,9 +970,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- request<E extends 'admin/roles/list', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/system-webhook/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -925,9 +982,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- request<E extends 'admin/roles/show', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/system-webhook/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -936,9 +994,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- request<E extends 'admin/roles/update', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/system-webhook/show', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -947,9 +1006,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
*/
- request<E extends 'admin/roles/assign', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/system-webhook/test', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -958,9 +1018,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- request<E extends 'admin/roles/unassign', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/system-webhook/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -969,9 +1030,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unnsfw-user*
*/
- request<E extends 'admin/roles/update-default-policies', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/unnsfw-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -980,9 +1041,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No* / **Permission**: *read:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar*
*/
- request<E extends 'admin/roles/users', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/unset-user-avatar', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -991,10 +1052,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner*
*/
- request<E extends 'admin/system-webhook/create', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/unset-user-banner', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1003,10 +1063,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unsilence-user*
*/
- request<E extends 'admin/system-webhook/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/unsilence-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1015,10 +1074,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user*
*/
- request<E extends 'admin/system-webhook/list', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/unsuspend-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1027,10 +1085,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
- request<E extends 'admin/system-webhook/show', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/update-abuse-user-report', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1039,10 +1096,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
*/
- request<E extends 'admin/system-webhook/update', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/update-meta', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1051,10 +1107,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:user-note*
*/
- request<E extends 'admin/system-webhook/test', P extends Endpoints[E]['req']>(
+ request<E extends 'admin/update-user-note', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1273,6 +1328,28 @@ declare module '../api.js' {
/**
* No description provided.
*
+ * **Credential required**: *No*
+ */
+ request<E extends 'bubble-game/ranking', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:account*
+ */
+ request<E extends 'bubble-game/register', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
* **Credential required**: *Yes* / **Permission**: *write:channels*
*/
request<E extends 'channels/create', P extends Endpoints[E]['req']>(
@@ -1284,9 +1361,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- request<E extends 'channels/featured', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/favorite', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1295,9 +1372,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- request<E extends 'channels/follow', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/featured', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1306,9 +1383,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:channels*
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- request<E extends 'channels/followed', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/follow', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1319,7 +1396,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:channels*
*/
- request<E extends 'channels/owned', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/followed', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1328,9 +1405,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:channels*
*/
- request<E extends 'channels/show', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/my-favorites', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1339,9 +1416,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:channels*
*/
- request<E extends 'channels/timeline', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/owned', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1350,9 +1427,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- request<E extends 'channels/unfollow', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/search', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1361,9 +1438,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- request<E extends 'channels/update', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/show', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1372,9 +1449,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- request<E extends 'channels/favorite', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/timeline', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1394,9 +1471,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:channels*
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- request<E extends 'channels/my-favorites', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/unfollow', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1405,9 +1482,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- request<E extends 'channels/search', P extends Endpoints[E]['req']>(
+ request<E extends 'channels/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1561,7 +1638,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'clips/remove-note', P extends Endpoints[E]['req']>(
+ request<E extends 'clips/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1572,7 +1649,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'clips/create', P extends Endpoints[E]['req']>(
+ request<E extends 'clips/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1581,9 +1658,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *Yes* / **Permission**: *write:clip-favorite*
*/
- request<E extends 'clips/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'clips/favorite', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1603,9 +1680,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *read:clip-favorite*
*/
- request<E extends 'clips/notes', P extends Endpoints[E]['req']>(
+ request<E extends 'clips/my-favorites', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1616,7 +1693,7 @@ declare module '../api.js' {
*
* **Credential required**: *No* / **Permission**: *read:account*
*/
- request<E extends 'clips/show', P extends Endpoints[E]['req']>(
+ request<E extends 'clips/notes', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1627,7 +1704,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'clips/update', P extends Endpoints[E]['req']>(
+ request<E extends 'clips/remove-note', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1636,9 +1713,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:clip-favorite*
+ * **Credential required**: *No* / **Permission**: *read:account*
*/
- request<E extends 'clips/favorite', P extends Endpoints[E]['req']>(
+ request<E extends 'clips/show', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1658,9 +1735,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:clip-favorite*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'clips/my-favorites', P extends Endpoints[E]['req']>(
+ request<E extends 'clips/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1733,22 +1810,22 @@ declare module '../api.js' {
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Search for a drive file by a hash of the contents.
+ * Search for a drive file by the given parameters.
*
* **Credential required**: *Yes* / **Permission**: *read:drive*
*/
- request<E extends 'drive/files/find-by-hash', P extends Endpoints[E]['req']>(
+ request<E extends 'drive/files/find', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Search for a drive file by the given parameters.
+ * Search for a drive file by a hash of the contents.
*
* **Credential required**: *Yes* / **Permission**: *read:drive*
*/
- request<E extends 'drive/files/find', P extends Endpoints[E]['req']>(
+ request<E extends 'drive/files/find-by-hash', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1880,6 +1957,28 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
+ request<E extends 'emoji', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ request<E extends 'emojis', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *No*
+ */
request<E extends 'endpoint', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
@@ -1958,6 +2057,17 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
+ request<E extends 'federation/stats', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *No*
+ */
request<E extends 'federation/update-remote-user', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
@@ -1978,9 +2088,21 @@ declare module '../api.js' {
/**
* No description provided.
*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
+ */
+ request<E extends 'fetch-external-resources', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
* **Credential required**: *No*
*/
- request<E extends 'federation/stats', P extends Endpoints[E]['req']>(
+ request<E extends 'fetch-rss', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -1989,9 +2111,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:following*
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
*/
- request<E extends 'following/create', P extends Endpoints[E]['req']>(
+ request<E extends 'flash/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2000,9 +2122,86 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:following*
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
*/
- request<E extends 'following/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'flash/delete', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ request<E extends 'flash/featured', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ */
+ request<E extends 'flash/like', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:flash*
+ */
+ request<E extends 'flash/my', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:flash-likes*
+ */
+ request<E extends 'flash/my-likes', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ request<E extends 'flash/show', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ */
+ request<E extends 'flash/unlike', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
+ */
+ request<E extends 'flash/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2013,7 +2212,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- request<E extends 'following/update', P extends Endpoints[E]['req']>(
+ request<E extends 'following/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2024,7 +2223,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- request<E extends 'following/update-all', P extends Endpoints[E]['req']>(
+ request<E extends 'following/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2077,6 +2276,17 @@ declare module '../api.js' {
/**
* No description provided.
*
+ * **Credential required**: *Yes* / **Permission**: *write:following*
+ */
+ request<E extends 'following/requests/reject', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
* **Credential required**: *Yes* / **Permission**: *read:following*
*/
request<E extends 'following/requests/sent', P extends Endpoints[E]['req']>(
@@ -2090,7 +2300,18 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- request<E extends 'following/requests/reject', P extends Endpoints[E]['req']>(
+ request<E extends 'following/update', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:following*
+ */
+ request<E extends 'following/update-all', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2200,7 +2421,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'get-online-users-count', P extends Endpoints[E]['req']>(
+ request<E extends 'get-avatar-decorations', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2211,7 +2432,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'get-avatar-decorations', P extends Endpoints[E]['req']>(
+ request<E extends 'get-online-users-count', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2325,7 +2546,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/2fa/register-key', P extends Endpoints[E]['req']>(
+ request<E extends 'i/2fa/register', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2337,7 +2558,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/2fa/register', P extends Endpoints[E]['req']>(
+ request<E extends 'i/2fa/register-key', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2349,7 +2570,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/2fa/update-key', P extends Endpoints[E]['req']>(
+ request<E extends 'i/2fa/remove-key', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2361,7 +2582,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/2fa/remove-key', P extends Endpoints[E]['req']>(
+ request<E extends 'i/2fa/unregister', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2373,7 +2594,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/2fa/unregister', P extends Endpoints[E]['req']>(
+ request<E extends 'i/2fa/update-key', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2406,9 +2627,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
*/
- request<E extends 'i/claim-achievement', P extends Endpoints[E]['req']>(
+ request<E extends 'i/change-password', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2417,10 +2639,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'i/change-password', P extends Endpoints[E]['req']>(
+ request<E extends 'i/claim-achievement', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2444,7 +2665,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/export-data', P extends Endpoints[E]['req']>(
+ request<E extends 'i/export-antennas', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2468,7 +2689,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/export-following', P extends Endpoints[E]['req']>(
+ request<E extends 'i/export-clips', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2480,7 +2701,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/export-mute', P extends Endpoints[E]['req']>(
+ request<E extends 'i/export-data', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2492,7 +2713,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/export-notes', P extends Endpoints[E]['req']>(
+ request<E extends 'i/export-favorites', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2504,7 +2725,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/export-clips', P extends Endpoints[E]['req']>(
+ request<E extends 'i/export-following', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2516,7 +2737,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/export-favorites', P extends Endpoints[E]['req']>(
+ request<E extends 'i/export-mute', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2528,7 +2749,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/export-user-lists', P extends Endpoints[E]['req']>(
+ request<E extends 'i/export-notes', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2540,7 +2761,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/export-antennas', P extends Endpoints[E]['req']>(
+ request<E extends 'i/export-user-lists', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2585,6 +2806,18 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
+ request<E extends 'i/import-antennas', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
+ */
request<E extends 'i/import-blocking', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
@@ -2609,7 +2842,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/import-notes', P extends Endpoints[E]['req']>(
+ request<E extends 'i/import-muting', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2621,7 +2854,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/import-muting', P extends Endpoints[E]['req']>(
+ request<E extends 'i/import-notes', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2645,7 +2878,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/import-antennas', P extends Endpoints[E]['req']>(
+ request<E extends 'i/move', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2745,7 +2978,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'i/registry/get-all', P extends Endpoints[E]['req']>(
+ request<E extends 'i/registry/get', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2756,7 +2989,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'i/registry/get-unsecure', P extends Endpoints[E]['req']>(
+ request<E extends 'i/registry/get-all', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2778,7 +3011,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'i/registry/get', P extends Endpoints[E]['req']>(
+ request<E extends 'i/registry/get-unsecure', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2789,7 +3022,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'i/registry/keys-with-type', P extends Endpoints[E]['req']>(
+ request<E extends 'i/registry/keys', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2800,7 +3033,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'i/registry/keys', P extends Endpoints[E]['req']>(
+ request<E extends 'i/registry/keys-with-type', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2878,18 +3111,6 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
- */
- request<E extends 'i/update-email', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * No description provided.
- *
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
request<E extends 'i/update', P extends Endpoints[E]['req']>(
@@ -2904,7 +3125,7 @@ declare module '../api.js' {
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- request<E extends 'i/move', P extends Endpoints[E]['req']>(
+ request<E extends 'i/update-email', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2924,9 +3145,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'i/webhooks/list', P extends Endpoints[E]['req']>(
+ request<E extends 'i/webhooks/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2937,7 +3158,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'i/webhooks/show', P extends Endpoints[E]['req']>(
+ request<E extends 'i/webhooks/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2946,9 +3167,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'i/webhooks/update', P extends Endpoints[E]['req']>(
+ request<E extends 'i/webhooks/show', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2957,9 +3178,10 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'i/webhooks/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'i/webhooks/test', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -2968,10 +3190,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'i/webhooks/test', P extends Endpoints[E]['req']>(
+ request<E extends 'i/webhooks/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3004,7 +3225,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:invite-codes*
*/
- request<E extends 'invite/list', P extends Endpoints[E]['req']>(
+ request<E extends 'invite/limit', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3015,7 +3236,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:invite-codes*
*/
- request<E extends 'invite/limit', P extends Endpoints[E]['req']>(
+ request<E extends 'invite/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3035,28 +3256,6 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
- */
- request<E extends 'emojis', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * No description provided.
- *
- * **Credential required**: *No*
- */
- request<E extends 'emoji', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * No description provided.
- *
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
@@ -3102,20 +3301,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:mutes*
- */
- request<E extends 'renote-mute/create', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:mutes*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'renote-mute/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'my/apps', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3124,9 +3312,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:mutes*
+ * **Credential required**: *No*
*/
- request<E extends 'renote-mute/list', P extends Endpoints[E]['req']>(
+ request<E extends 'notes', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3135,9 +3323,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *No*
*/
- request<E extends 'my/apps', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/bubble-timeline', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3148,7 +3336,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'notes', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/children', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3159,7 +3347,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'notes/children', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/clips', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3170,7 +3358,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'notes/clips', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/conversation', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3179,9 +3367,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
*/
- request<E extends 'notes/conversation', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3192,7 +3380,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:notes*
*/
- request<E extends 'notes/create', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3203,7 +3391,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:notes*
*/
- request<E extends 'notes/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/edit', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3267,9 +3455,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'notes/bubble-timeline', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/hybrid-timeline', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3278,9 +3466,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:reactions*
*/
- request<E extends 'notes/hybrid-timeline', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/like', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3322,9 +3510,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:votes*
+ * **Credential required**: *Yes* / **Permission**: *read:federation*
*/
- request<E extends 'notes/polls/vote', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/polls/refresh', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3333,9 +3521,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:federation*
+ * **Credential required**: *Yes* / **Permission**: *write:votes*
*/
- request<E extends 'notes/polls/refresh', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/polls/vote', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3377,17 +3565,6 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:reactions*
- */
- request<E extends 'notes/like', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * No description provided.
- *
* **Credential required**: *No*
*/
request<E extends 'notes/renotes', P extends Endpoints[E]['req']>(
@@ -3445,7 +3622,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'notes/search-by-tag', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/search', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3456,7 +3633,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'notes/search', P extends Endpoints[E]['req']>(
+ request<E extends 'notes/search-by-tag', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3553,17 +3730,6 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:notes*
- */
- request<E extends 'notes/edit', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * No description provided.
- *
* **Credential required**: *No*
*/
request<E extends 'notes/versions', P extends Endpoints[E]['req']>(
@@ -3708,9 +3874,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *No*
*/
- request<E extends 'flash/create', P extends Endpoints[E]['req']>(
+ request<E extends 'ping', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3719,9 +3885,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *No*
*/
- request<E extends 'flash/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'pinned-users', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3730,9 +3896,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'flash/featured', P extends Endpoints[E]['req']>(
+ request<E extends 'promo/read', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3741,9 +3907,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ * **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
- request<E extends 'flash/like', P extends Endpoints[E]['req']>(
+ request<E extends 'renote-mute/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3752,9 +3918,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
- request<E extends 'flash/show', P extends Endpoints[E]['req']>(
+ request<E extends 'renote-mute/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3763,42 +3929,42 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ * **Credential required**: *Yes* / **Permission**: *read:mutes*
*/
- request<E extends 'flash/unlike', P extends Endpoints[E]['req']>(
+ request<E extends 'renote-mute/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Request a users password to be reset.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *No*
*/
- request<E extends 'flash/update', P extends Endpoints[E]['req']>(
+ request<E extends 'request-reset-password', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis.
*
- * **Credential required**: *Yes* / **Permission**: *read:flash*
+ * **Credential required**: *No*
*/
- request<E extends 'flash/my', P extends Endpoints[E]['req']>(
+ request<E extends 'reset-db', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Complete the password reset that was previously requested.
*
- * **Credential required**: *Yes* / **Permission**: *read:flash-likes*
+ * **Credential required**: *No*
*/
- request<E extends 'flash/my-likes', P extends Endpoints[E]['req']>(
+ request<E extends 'reset-password', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3809,7 +3975,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'ping', P extends Endpoints[E]['req']>(
+ request<E extends 'retention', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3818,9 +3984,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'pinned-users', P extends Endpoints[E]['req']>(
+ request<E extends 'reversi/cancel-match', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3829,9 +3995,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *No*
*/
- request<E extends 'promo/read', P extends Endpoints[E]['req']>(
+ request<E extends 'reversi/games', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3842,7 +4008,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'roles/list', P extends Endpoints[E]['req']>(
+ request<E extends 'reversi/invitations', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3851,9 +4017,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'roles/show', P extends Endpoints[E]['req']>(
+ request<E extends 'reversi/match', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3864,7 +4030,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'roles/users', P extends Endpoints[E]['req']>(
+ request<E extends 'reversi/show-game', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3873,42 +4039,42 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'roles/notes', P extends Endpoints[E]['req']>(
+ request<E extends 'reversi/surrender', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Request a users password to be reset.
+ * No description provided.
*
* **Credential required**: *No*
*/
- request<E extends 'request-reset-password', P extends Endpoints[E]['req']>(
+ request<E extends 'reversi/verify', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis.
+ * No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'reset-db', P extends Endpoints[E]['req']>(
+ request<E extends 'roles/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Complete the password reset that was previously requested.
+ * No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'reset-password', P extends Endpoints[E]['req']>(
+ request<E extends 'roles/notes', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3919,7 +4085,7 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'server-info', P extends Endpoints[E]['req']>(
+ request<E extends 'roles/show', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -3930,142 +4096,142 @@ declare module '../api.js' {
*
* **Credential required**: *No*
*/
- request<E extends 'stats', P extends Endpoints[E]['req']>(
+ request<E extends 'roles/users', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Check push notification registration exists.
+ * No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
+ * **Credential required**: *No*
*/
- request<E extends 'sw/show-registration', P extends Endpoints[E]['req']>(
+ request<E extends 'server-info', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Update push notification registration.
+ * Get Sharkey Sponsors or Instance Sponsors
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
+ * **Credential required**: *No*
*/
- request<E extends 'sw/update-registration', P extends Endpoints[E]['req']>(
+ request<E extends 'sponsors', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Register to receive push notifications.
+ * No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
+ * **Credential required**: *No*
*/
- request<E extends 'sw/register', P extends Endpoints[E]['req']>(
+ request<E extends 'stats', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Unregister from receiving push notifications.
+ * Register to receive push notifications.
*
- * **Credential required**: *No*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
*/
- request<E extends 'sw/unregister', P extends Endpoints[E]['req']>(
+ request<E extends 'sw/register', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Endpoint for testing input validation.
+ * Check push notification registration exists.
*
- * **Credential required**: *No*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
*/
- request<E extends 'test', P extends Endpoints[E]['req']>(
+ request<E extends 'sw/show-registration', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Unregister from receiving push notifications.
*
* **Credential required**: *No*
*/
- request<E extends 'username/available', P extends Endpoints[E]['req']>(
+ request<E extends 'sw/unregister', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Update push notification registration.
*
- * **Credential required**: *No*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
*/
- request<E extends 'users', P extends Endpoints[E]['req']>(
+ request<E extends 'sw/update-registration', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Show all clips this user owns.
+ * Endpoint for testing input validation.
*
* **Credential required**: *No*
*/
- request<E extends 'users/clips', P extends Endpoints[E]['req']>(
+ request<E extends 'test', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Show everyone that follows this user.
+ * No description provided.
*
* **Credential required**: *No*
*/
- request<E extends 'users/followers', P extends Endpoints[E]['req']>(
+ request<E extends 'username/available', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Show everyone that this user is following.
+ * No description provided.
*
* **Credential required**: *No*
*/
- request<E extends 'users/following', P extends Endpoints[E]['req']>(
+ request<E extends 'users', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Show all gallery posts by the given user.
+ * No description provided.
*
* **Credential required**: *No*
*/
- request<E extends 'users/gallery/posts', P extends Endpoints[E]['req']>(
+ request<E extends 'users/achievements', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Get a list of other users that the specified user frequently replies to.
+ * Show all clips this user owns.
*
* **Credential required**: *No*
*/
- request<E extends 'users/get-frequently-replied-users', P extends Endpoints[E]['req']>(
+ request<E extends 'users/clips', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4083,77 +4249,66 @@ declare module '../api.js' {
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Create a new list of users.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- request<E extends 'users/lists/create', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * Delete an existing list of users.
+ * Show all flashs this user created.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *No*
*/
- request<E extends 'users/lists/delete', P extends Endpoints[E]['req']>(
+ request<E extends 'users/flashs', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Show all lists that the authenticated user has created.
+ * Show everyone that follows this user.
*
- * **Credential required**: *No* / **Permission**: *read:account*
+ * **Credential required**: *No*
*/
- request<E extends 'users/lists/list', P extends Endpoints[E]['req']>(
+ request<E extends 'users/followers', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Remove a user from a list.
+ * Show everyone that this user is following.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *No*
*/
- request<E extends 'users/lists/pull', P extends Endpoints[E]['req']>(
+ request<E extends 'users/following', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Add a user to an existing list.
+ * Show all gallery posts by the given user.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *No*
*/
- request<E extends 'users/lists/push', P extends Endpoints[E]['req']>(
+ request<E extends 'users/gallery/posts', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Show the properties of a list.
+ * Get a list of other users that the specified user frequently replies to.
*
- * **Credential required**: *No* / **Permission**: *read:account*
+ * **Credential required**: *No*
*/
- request<E extends 'users/lists/show', P extends Endpoints[E]['req']>(
+ request<E extends 'users/get-frequently-replied-users', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Create a new list of users.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'users/lists/favorite', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4164,29 +4319,18 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'users/lists/unfavorite', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * Update the properties of a list.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- request<E extends 'users/lists/update', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/create-from-public', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Delete an existing list of users.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'users/lists/create-from-public', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4197,7 +4341,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'users/lists/update-membership', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/favorite', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4215,110 +4359,44 @@ declare module '../api.js' {
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
- *
- * **Credential required**: *No*
- */
- request<E extends 'users/notes', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * Show all pages this user created.
- *
- * **Credential required**: *No*
- */
- request<E extends 'users/pages', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * Show all flashs this user created.
- *
- * **Credential required**: *No*
- */
- request<E extends 'users/flashs', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * Show all reactions this user made.
- *
- * **Credential required**: *No*
- */
- request<E extends 'users/reactions', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * Show users that the authenticated user might be interested to follow.
- *
- * **Credential required**: *Yes* / **Permission**: *read:account*
- */
- request<E extends 'users/recommendation', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * Show the different kinds of relations between the authenticated user and the specified user(s).
- *
- * **Credential required**: *Yes* / **Permission**: *read:account*
- */
- request<E extends 'users/relation', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * File a report.
+ * Show all lists that the authenticated user has created.
*
- * **Credential required**: *Yes* / **Permission**: *write:report-abuse*
+ * **Credential required**: *No* / **Permission**: *read:account*
*/
- request<E extends 'users/report-abuse', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Search for a user by username and/or host.
+ * Remove a user from a list.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'users/search-by-username-and-host', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/pull', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Search for users.
+ * Add a user to an existing list.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'users/search', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/push', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Show the properties of a user.
+ * Show the properties of a list.
*
- * **Credential required**: *No*
+ * **Credential required**: *No* / **Permission**: *read:account*
*/
- request<E extends 'users/show', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/show', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4327,20 +4405,20 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'users/achievements', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/unfavorite', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Update the properties of a list.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'users/update-memo', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4349,9 +4427,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'fetch-rss', P extends Endpoints[E]['req']>(
+ request<E extends 'users/lists/update-membership', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4360,109 +4438,97 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
+ * **Credential required**: *No*
*/
- request<E extends 'fetch-external-resources', P extends Endpoints[E]['req']>(
+ request<E extends 'users/notes', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Show all pages this user created.
*
* **Credential required**: *No*
*/
- request<E extends 'retention', P extends Endpoints[E]['req']>(
+ request<E extends 'users/pages', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * Get Sharkey Sponsors or Instance Sponsors
+ * Show all reactions this user made.
*
* **Credential required**: *No*
*/
- request<E extends 'sponsors', P extends Endpoints[E]['req']>(
+ request<E extends 'users/reactions', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Show users that the authenticated user might be interested to follow.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'bubble-game/register', P extends Endpoints[E]['req']>(
+ request<E extends 'users/recommendation', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Show the different kinds of relations between the authenticated user and the specified user(s).
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request<E extends 'bubble-game/ranking', P extends Endpoints[E]['req']>(
+ request<E extends 'users/relation', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * File a report.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *Yes* / **Permission**: *write:report-abuse*
*/
- request<E extends 'reversi/cancel-match', P extends Endpoints[E]['req']>(
+ request<E extends 'users/report-abuse', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Search for users.
*
* **Credential required**: *No*
*/
- request<E extends 'reversi/games', P extends Endpoints[E]['req']>(
- endpoint: E,
- params: P,
- credential?: string | null,
- ): Promise<SwitchCaseResponseType<E, P>>;
-
- /**
- * No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- request<E extends 'reversi/match', P extends Endpoints[E]['req']>(
+ request<E extends 'users/search', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Search for a user by username and/or host.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *No*
*/
- request<E extends 'reversi/invitations', P extends Endpoints[E]['req']>(
+ request<E extends 'users/search-by-username-and-host', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
- * No description provided.
+ * Show the properties of a user.
*
* **Credential required**: *No*
*/
- request<E extends 'reversi/show-game', P extends Endpoints[E]['req']>(
+ request<E extends 'users/show', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4473,7 +4539,7 @@ declare module '../api.js' {
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- request<E extends 'reversi/surrender', P extends Endpoints[E]['req']>(
+ request<E extends 'users/update-memo', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
@@ -4482,9 +4548,9 @@ declare module '../api.js' {
/**
* No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- request<E extends 'reversi/verify', P extends Endpoints[E]['req']>(
+ request<E extends 'v2/admin/emoji/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 66e7126460..0ef2db70f6 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -1,18 +1,17 @@
import type {
EmptyRequest,
EmptyResponse,
- AdminMetaResponse,
- AdminAbuseUserReportsRequest,
- AdminAbuseUserReportsResponse,
+ AdminAbuseReportNotificationRecipientCreateRequest,
+ AdminAbuseReportNotificationRecipientCreateResponse,
+ AdminAbuseReportNotificationRecipientDeleteRequest,
AdminAbuseReportNotificationRecipientListRequest,
AdminAbuseReportNotificationRecipientListResponse,
AdminAbuseReportNotificationRecipientShowRequest,
AdminAbuseReportNotificationRecipientShowResponse,
- AdminAbuseReportNotificationRecipientCreateRequest,
- AdminAbuseReportNotificationRecipientCreateResponse,
AdminAbuseReportNotificationRecipientUpdateRequest,
AdminAbuseReportNotificationRecipientUpdateResponse,
- AdminAbuseReportNotificationRecipientDeleteRequest,
+ AdminAbuseUserReportsRequest,
+ AdminAbuseUserReportsResponse,
AdminAccountsCreateRequest,
AdminAccountsCreateResponse,
AdminAccountsDeleteRequest,
@@ -30,31 +29,35 @@ import type {
AdminAnnouncementsListRequest,
AdminAnnouncementsListResponse,
AdminAnnouncementsUpdateRequest,
+ AdminApproveUserRequest,
AdminAvatarDecorationsCreateRequest,
AdminAvatarDecorationsCreateResponse,
AdminAvatarDecorationsDeleteRequest,
AdminAvatarDecorationsListRequest,
AdminAvatarDecorationsListResponse,
AdminAvatarDecorationsUpdateRequest,
+ AdminCaptchaCurrentResponse,
+ AdminCaptchaSaveRequest,
+ AdminCwUserRequest,
+ AdminDeclineUserRequest,
+ AdminDeleteAccountRequest,
AdminDeleteAllFilesOfAUserRequest,
- AdminUnsetUserAvatarRequest,
- AdminUnsetUserBannerRequest,
AdminDriveFilesRequest,
AdminDriveFilesResponse,
AdminDriveShowFileRequest,
AdminDriveShowFileResponse,
- AdminEmojiAddAliasesBulkRequest,
AdminEmojiAddRequest,
AdminEmojiAddResponse,
+ AdminEmojiAddAliasesBulkRequest,
AdminEmojiCopyRequest,
AdminEmojiCopyResponse,
- AdminEmojiDeleteBulkRequest,
AdminEmojiDeleteRequest,
+ AdminEmojiDeleteBulkRequest,
AdminEmojiImportZipRequest,
- AdminEmojiListRemoteRequest,
- AdminEmojiListRemoteResponse,
AdminEmojiListRequest,
AdminEmojiListResponse,
+ AdminEmojiListRemoteRequest,
+ AdminEmojiListRemoteResponse,
AdminEmojiRemoveAliasesBulkRequest,
AdminEmojiSetAliasesBulkRequest,
AdminEmojiSetCategoryBulkRequest,
@@ -64,6 +67,7 @@ import type {
AdminFederationRefreshRemoteInstanceMetadataRequest,
AdminFederationRemoveAllFollowingRequest,
AdminFederationUpdateInstanceRequest,
+ AdminForwardAbuseUserReportRequest,
AdminGetIndexStatsResponse,
AdminGetTableStatsResponse,
AdminGetUserIpsRequest,
@@ -72,11 +76,14 @@ import type {
AdminInviteCreateResponse,
AdminInviteListRequest,
AdminInviteListResponse,
+ AdminMetaResponse,
+ AdminNsfwUserRequest,
AdminPromoCreateRequest,
AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse,
AdminQueuePromoteRequest,
AdminQueueStatsResponse,
+ AdminRejectQuotesRequest,
AdminRelaysAddRequest,
AdminRelaysAddResponse,
AdminRelaysListResponse,
@@ -84,39 +91,28 @@ import type {
AdminResetPasswordRequest,
AdminResetPasswordResponse,
AdminResolveAbuseUserReportRequest,
- AdminForwardAbuseUserReportRequest,
- AdminUpdateAbuseUserReportRequest,
- AdminSendEmailRequest,
- AdminServerInfoResponse,
- AdminShowModerationLogsRequest,
- AdminShowModerationLogsResponse,
- AdminShowUserRequest,
- AdminShowUserResponse,
- AdminShowUsersRequest,
- AdminShowUsersResponse,
- AdminNsfwUserRequest,
- AdminUnnsfwUserRequest,
- AdminSilenceUserRequest,
- AdminUnsilenceUserRequest,
- AdminSuspendUserRequest,
- AdminApproveUserRequest,
- AdminDeclineUserRequest,
- AdminUnsuspendUserRequest,
- AdminUpdateMetaRequest,
- AdminDeleteAccountRequest,
- AdminUpdateUserNoteRequest,
+ AdminRolesAssignRequest,
AdminRolesCreateRequest,
AdminRolesCreateResponse,
AdminRolesDeleteRequest,
AdminRolesListResponse,
AdminRolesShowRequest,
AdminRolesShowResponse,
- AdminRolesUpdateRequest,
- AdminRolesAssignRequest,
AdminRolesUnassignRequest,
+ AdminRolesUpdateRequest,
AdminRolesUpdateDefaultPoliciesRequest,
AdminRolesUsersRequest,
AdminRolesUsersResponse,
+ AdminSendEmailRequest,
+ AdminServerInfoResponse,
+ AdminShowModerationLogsRequest,
+ AdminShowModerationLogsResponse,
+ AdminShowUserRequest,
+ AdminShowUserResponse,
+ AdminShowUsersRequest,
+ AdminShowUsersResponse,
+ AdminSilenceUserRequest,
+ AdminSuspendUserRequest,
AdminSystemWebhookCreateRequest,
AdminSystemWebhookCreateResponse,
AdminSystemWebhookDeleteRequest,
@@ -124,9 +120,17 @@ import type {
AdminSystemWebhookListResponse,
AdminSystemWebhookShowRequest,
AdminSystemWebhookShowResponse,
+ AdminSystemWebhookTestRequest,
AdminSystemWebhookUpdateRequest,
AdminSystemWebhookUpdateResponse,
- AdminSystemWebhookTestRequest,
+ AdminUnnsfwUserRequest,
+ AdminUnsetUserAvatarRequest,
+ AdminUnsetUserBannerRequest,
+ AdminUnsilenceUserRequest,
+ AdminUnsuspendUserRequest,
+ AdminUpdateAbuseUserReportRequest,
+ AdminUpdateMetaRequest,
+ AdminUpdateUserNoteRequest,
AnnouncementsRequest,
AnnouncementsResponse,
AnnouncementsShowRequest,
@@ -162,26 +166,29 @@ import type {
BlockingDeleteResponse,
BlockingListRequest,
BlockingListResponse,
+ BubbleGameRankingRequest,
+ BubbleGameRankingResponse,
+ BubbleGameRegisterRequest,
ChannelsCreateRequest,
ChannelsCreateResponse,
+ ChannelsFavoriteRequest,
ChannelsFeaturedResponse,
ChannelsFollowRequest,
ChannelsFollowedRequest,
ChannelsFollowedResponse,
+ ChannelsMyFavoritesResponse,
ChannelsOwnedRequest,
ChannelsOwnedResponse,
+ ChannelsSearchRequest,
+ ChannelsSearchResponse,
ChannelsShowRequest,
ChannelsShowResponse,
ChannelsTimelineRequest,
ChannelsTimelineResponse,
+ ChannelsUnfavoriteRequest,
ChannelsUnfollowRequest,
ChannelsUpdateRequest,
ChannelsUpdateResponse,
- ChannelsFavoriteRequest,
- ChannelsUnfavoriteRequest,
- ChannelsMyFavoritesResponse,
- ChannelsSearchRequest,
- ChannelsSearchResponse,
ChartsActiveUsersRequest,
ChartsActiveUsersResponse,
ChartsApRequestRequest,
@@ -207,20 +214,20 @@ import type {
ChartsUsersRequest,
ChartsUsersResponse,
ClipsAddNoteRequest,
- ClipsRemoveNoteRequest,
ClipsCreateRequest,
ClipsCreateResponse,
ClipsDeleteRequest,
+ ClipsFavoriteRequest,
ClipsListResponse,
+ ClipsMyFavoritesResponse,
ClipsNotesRequest,
ClipsNotesResponse,
+ ClipsRemoveNoteRequest,
ClipsShowRequest,
ClipsShowResponse,
+ ClipsUnfavoriteRequest,
ClipsUpdateRequest,
ClipsUpdateResponse,
- ClipsFavoriteRequest,
- ClipsUnfavoriteRequest,
- ClipsMyFavoritesResponse,
DriveResponse,
DriveFilesRequest,
DriveFilesResponse,
@@ -231,10 +238,10 @@ import type {
DriveFilesCreateRequest,
DriveFilesCreateResponse,
DriveFilesDeleteRequest,
- DriveFilesFindByHashRequest,
- DriveFilesFindByHashResponse,
DriveFilesFindRequest,
DriveFilesFindResponse,
+ DriveFilesFindByHashRequest,
+ DriveFilesFindByHashResponse,
DriveFilesShowRequest,
DriveFilesShowResponse,
DriveFilesUpdateRequest,
@@ -255,6 +262,9 @@ import type {
DriveStreamResponse,
EmailAddressAvailableRequest,
EmailAddressAvailableResponse,
+ EmojiRequest,
+ EmojiResponse,
+ EmojisResponse,
EndpointRequest,
EndpointResponse,
EndpointsResponse,
@@ -266,18 +276,33 @@ import type {
FederationInstancesResponse,
FederationShowInstanceRequest,
FederationShowInstanceResponse,
+ FederationStatsRequest,
+ FederationStatsResponse,
FederationUpdateRemoteUserRequest,
FederationUsersRequest,
FederationUsersResponse,
- FederationStatsRequest,
- FederationStatsResponse,
+ FetchExternalResourcesRequest,
+ FetchExternalResourcesResponse,
+ FetchRssRequest,
+ FetchRssResponse,
+ FlashCreateRequest,
+ FlashCreateResponse,
+ FlashDeleteRequest,
+ FlashFeaturedRequest,
+ FlashFeaturedResponse,
+ FlashLikeRequest,
+ FlashMyRequest,
+ FlashMyResponse,
+ FlashMyLikesRequest,
+ FlashMyLikesResponse,
+ FlashShowRequest,
+ FlashShowResponse,
+ FlashUnlikeRequest,
+ FlashUpdateRequest,
FollowingCreateRequest,
FollowingCreateResponse,
FollowingDeleteRequest,
FollowingDeleteResponse,
- FollowingUpdateRequest,
- FollowingUpdateResponse,
- FollowingUpdateAllRequest,
FollowingInvalidateRequest,
FollowingInvalidateResponse,
FollowingRequestsAcceptRequest,
@@ -285,9 +310,12 @@ import type {
FollowingRequestsCancelResponse,
FollowingRequestsListRequest,
FollowingRequestsListResponse,
+ FollowingRequestsRejectRequest,
FollowingRequestsSentRequest,
FollowingRequestsSentResponse,
- FollowingRequestsRejectRequest,
+ FollowingUpdateRequest,
+ FollowingUpdateResponse,
+ FollowingUpdateAllRequest,
GalleryFeaturedRequest,
GalleryFeaturedResponse,
GalleryPopularResponse,
@@ -302,8 +330,8 @@ import type {
GalleryPostsUnlikeRequest,
GalleryPostsUpdateRequest,
GalleryPostsUpdateResponse,
- GetOnlineUsersCountResponse,
GetAvatarDecorationsResponse,
+ GetOnlineUsersCountResponse,
HashtagsListRequest,
HashtagsListResponse,
HashtagsSearchRequest,
@@ -319,19 +347,19 @@ import type {
I2faKeyDoneRequest,
I2faKeyDoneResponse,
I2faPasswordLessRequest,
- I2faRegisterKeyRequest,
- I2faRegisterKeyResponse,
I2faRegisterRequest,
I2faRegisterResponse,
- I2faUpdateKeyRequest,
+ I2faRegisterKeyRequest,
+ I2faRegisterKeyResponse,
I2faRemoveKeyRequest,
I2faUnregisterRequest,
+ I2faUpdateKeyRequest,
IAppsRequest,
IAppsResponse,
IAuthorizedAppsRequest,
IAuthorizedAppsResponse,
- IClaimAchievementRequest,
IChangePasswordRequest,
+ IClaimAchievementRequest,
IDeleteAccountRequest,
IExportFollowingRequest,
IFavoritesRequest,
@@ -340,12 +368,14 @@ import type {
IGalleryLikesResponse,
IGalleryPostsRequest,
IGalleryPostsResponse,
+ IImportAntennasRequest,
IImportBlockingRequest,
IImportFollowingRequest,
- IImportNotesRequest,
IImportMutingRequest,
+ IImportNotesRequest,
IImportUserListsRequest,
- IImportAntennasRequest,
+ IMoveRequest,
+ IMoveResponse,
INotificationsRequest,
INotificationsResponse,
INotificationsGroupedRequest,
@@ -358,17 +388,17 @@ import type {
IPinResponse,
IReadAnnouncementRequest,
IRegenerateTokenRequest,
+ IRegistryGetRequest,
+ IRegistryGetResponse,
IRegistryGetAllRequest,
IRegistryGetAllResponse,
- IRegistryGetUnsecureRequest,
IRegistryGetDetailRequest,
IRegistryGetDetailResponse,
- IRegistryGetRequest,
- IRegistryGetResponse,
- IRegistryKeysWithTypeRequest,
- IRegistryKeysWithTypeResponse,
+ IRegistryGetUnsecureRequest,
IRegistryKeysRequest,
IRegistryKeysResponse,
+ IRegistryKeysWithTypeRequest,
+ IRegistryKeysWithTypeResponse,
IRegistryRemoveRequest,
IRegistryScopesWithDomainResponse,
IRegistrySetRequest,
@@ -377,44 +407,37 @@ import type {
ISigninHistoryResponse,
IUnpinRequest,
IUnpinResponse,
- IUpdateEmailRequest,
- IUpdateEmailResponse,
IUpdateRequest,
IUpdateResponse,
- IMoveRequest,
- IMoveResponse,
+ IUpdateEmailRequest,
+ IUpdateEmailResponse,
IWebhooksCreateRequest,
IWebhooksCreateResponse,
+ IWebhooksDeleteRequest,
IWebhooksListResponse,
IWebhooksShowRequest,
IWebhooksShowResponse,
- IWebhooksUpdateRequest,
- IWebhooksDeleteRequest,
IWebhooksTestRequest,
+ IWebhooksUpdateRequest,
InviteCreateResponse,
InviteDeleteRequest,
+ InviteLimitResponse,
InviteListRequest,
InviteListResponse,
- InviteLimitResponse,
MetaRequest,
MetaResponse,
- EmojisResponse,
- EmojiRequest,
- EmojiResponse,
MiauthGenTokenRequest,
MiauthGenTokenResponse,
MuteCreateRequest,
MuteDeleteRequest,
MuteListRequest,
MuteListResponse,
- RenoteMuteCreateRequest,
- RenoteMuteDeleteRequest,
- RenoteMuteListRequest,
- RenoteMuteListResponse,
MyAppsRequest,
MyAppsResponse,
NotesRequest,
NotesResponse,
+ NotesBubbleTimelineRequest,
+ NotesBubbleTimelineResponse,
NotesChildrenRequest,
NotesChildrenResponse,
NotesClipsRequest,
@@ -424,6 +447,8 @@ import type {
NotesCreateRequest,
NotesCreateResponse,
NotesDeleteRequest,
+ NotesEditRequest,
+ NotesEditResponse,
NotesFavoritesCreateRequest,
NotesFavoritesDeleteRequest,
NotesFeaturedRequest,
@@ -432,23 +457,21 @@ import type {
NotesFollowingResponse,
NotesGlobalTimelineRequest,
NotesGlobalTimelineResponse,
- NotesBubbleTimelineRequest,
- NotesBubbleTimelineResponse,
NotesHybridTimelineRequest,
NotesHybridTimelineResponse,
+ NotesLikeRequest,
NotesLocalTimelineRequest,
NotesLocalTimelineResponse,
NotesMentionsRequest,
NotesMentionsResponse,
NotesPollsRecommendationRequest,
NotesPollsRecommendationResponse,
- NotesPollsVoteRequest,
NotesPollsRefreshRequest,
+ NotesPollsVoteRequest,
NotesReactionsRequest,
NotesReactionsResponse,
NotesReactionsCreateRequest,
NotesReactionsDeleteRequest,
- NotesLikeRequest,
NotesRenotesRequest,
NotesRenotesResponse,
NotesRepliesRequest,
@@ -457,10 +480,10 @@ import type {
NotesScheduleDeleteRequest,
NotesScheduleListRequest,
NotesScheduleListResponse,
- NotesSearchByTagRequest,
- NotesSearchByTagResponse,
NotesSearchRequest,
NotesSearchResponse,
+ NotesSearchByTagRequest,
+ NotesSearchByTagResponse,
NotesShowRequest,
NotesShowResponse,
NotesStateRequest,
@@ -474,8 +497,6 @@ import type {
NotesUnrenoteRequest,
NotesUserListTimelineRequest,
NotesUserListTimelineResponse,
- NotesEditRequest,
- NotesEditResponse,
NotesVersionsRequest,
NotesVersionsResponse,
NotificationsCreateRequest,
@@ -489,49 +510,58 @@ import type {
PagesShowResponse,
PagesUnlikeRequest,
PagesUpdateRequest,
- FlashCreateRequest,
- FlashCreateResponse,
- FlashDeleteRequest,
- FlashFeaturedRequest,
- FlashFeaturedResponse,
- FlashLikeRequest,
- FlashShowRequest,
- FlashShowResponse,
- FlashUnlikeRequest,
- FlashUpdateRequest,
- FlashMyRequest,
- FlashMyResponse,
- FlashMyLikesRequest,
- FlashMyLikesResponse,
PingResponse,
PinnedUsersResponse,
PromoReadRequest,
+ RenoteMuteCreateRequest,
+ RenoteMuteDeleteRequest,
+ RenoteMuteListRequest,
+ RenoteMuteListResponse,
+ RequestResetPasswordRequest,
+ ResetPasswordRequest,
+ RetentionResponse,
+ ReversiCancelMatchRequest,
+ ReversiGamesRequest,
+ ReversiGamesResponse,
+ ReversiInvitationsResponse,
+ ReversiMatchRequest,
+ ReversiMatchResponse,
+ ReversiShowGameRequest,
+ ReversiShowGameResponse,
+ ReversiSurrenderRequest,
+ ReversiVerifyRequest,
+ ReversiVerifyResponse,
RolesListResponse,
+ RolesNotesRequest,
+ RolesNotesResponse,
RolesShowRequest,
RolesShowResponse,
RolesUsersRequest,
RolesUsersResponse,
- RolesNotesRequest,
- RolesNotesResponse,
- RequestResetPasswordRequest,
- ResetPasswordRequest,
ServerInfoResponse,
+ SponsorsRequest,
StatsResponse,
+ SwRegisterRequest,
+ SwRegisterResponse,
SwShowRegistrationRequest,
SwShowRegistrationResponse,
+ SwUnregisterRequest,
SwUpdateRegistrationRequest,
SwUpdateRegistrationResponse,
- SwRegisterRequest,
- SwRegisterResponse,
- SwUnregisterRequest,
TestRequest,
TestResponse,
UsernameAvailableRequest,
UsernameAvailableResponse,
UsersRequest,
UsersResponse,
+ UsersAchievementsRequest,
+ UsersAchievementsResponse,
UsersClipsRequest,
UsersClipsResponse,
+ UsersFeaturedNotesRequest,
+ UsersFeaturedNotesResponse,
+ UsersFlashsRequest,
+ UsersFlashsResponse,
UsersFollowersRequest,
UsersFollowersResponse,
UsersFollowingRequest,
@@ -540,32 +570,28 @@ import type {
UsersGalleryPostsResponse,
UsersGetFrequentlyRepliedUsersRequest,
UsersGetFrequentlyRepliedUsersResponse,
- UsersFeaturedNotesRequest,
- UsersFeaturedNotesResponse,
UsersListsCreateRequest,
UsersListsCreateResponse,
+ UsersListsCreateFromPublicRequest,
+ UsersListsCreateFromPublicResponse,
UsersListsDeleteRequest,
+ UsersListsFavoriteRequest,
+ UsersListsGetMembershipsRequest,
+ UsersListsGetMembershipsResponse,
UsersListsListRequest,
UsersListsListResponse,
UsersListsPullRequest,
UsersListsPushRequest,
UsersListsShowRequest,
UsersListsShowResponse,
- UsersListsFavoriteRequest,
UsersListsUnfavoriteRequest,
UsersListsUpdateRequest,
UsersListsUpdateResponse,
- UsersListsCreateFromPublicRequest,
- UsersListsCreateFromPublicResponse,
UsersListsUpdateMembershipRequest,
- UsersListsGetMembershipsRequest,
- UsersListsGetMembershipsResponse,
UsersNotesRequest,
UsersNotesResponse,
UsersPagesRequest,
UsersPagesResponse,
- UsersFlashsRequest,
- UsersFlashsResponse,
UsersReactionsRequest,
UsersReactionsResponse,
UsersRecommendationRequest,
@@ -573,45 +599,24 @@ import type {
UsersRelationRequest,
UsersRelationResponse,
UsersReportAbuseRequest,
- UsersSearchByUsernameAndHostRequest,
- UsersSearchByUsernameAndHostResponse,
UsersSearchRequest,
UsersSearchResponse,
+ UsersSearchByUsernameAndHostRequest,
+ UsersSearchByUsernameAndHostResponse,
UsersShowRequest,
UsersShowResponse,
- UsersAchievementsRequest,
- UsersAchievementsResponse,
UsersUpdateMemoRequest,
- FetchRssRequest,
- FetchRssResponse,
- FetchExternalResourcesRequest,
- FetchExternalResourcesResponse,
- RetentionResponse,
- SponsorsRequest,
- BubbleGameRegisterRequest,
- BubbleGameRankingRequest,
- BubbleGameRankingResponse,
- ReversiCancelMatchRequest,
- ReversiGamesRequest,
- ReversiGamesResponse,
- ReversiMatchRequest,
- ReversiMatchResponse,
- ReversiInvitationsResponse,
- ReversiShowGameRequest,
- ReversiShowGameResponse,
- ReversiSurrenderRequest,
- ReversiVerifyRequest,
- ReversiVerifyResponse,
+ V2AdminEmojiListRequest,
+ V2AdminEmojiListResponse,
} from './entities.js';
export type Endpoints = {
- 'admin/meta': { req: EmptyRequest; res: AdminMetaResponse };
- 'admin/abuse-user-reports': { req: AdminAbuseUserReportsRequest; res: AdminAbuseUserReportsResponse };
+ 'admin/abuse-report/notification-recipient/create': { req: AdminAbuseReportNotificationRecipientCreateRequest; res: AdminAbuseReportNotificationRecipientCreateResponse };
+ 'admin/abuse-report/notification-recipient/delete': { req: AdminAbuseReportNotificationRecipientDeleteRequest; res: EmptyResponse };
'admin/abuse-report/notification-recipient/list': { req: AdminAbuseReportNotificationRecipientListRequest; res: AdminAbuseReportNotificationRecipientListResponse };
'admin/abuse-report/notification-recipient/show': { req: AdminAbuseReportNotificationRecipientShowRequest; res: AdminAbuseReportNotificationRecipientShowResponse };
- 'admin/abuse-report/notification-recipient/create': { req: AdminAbuseReportNotificationRecipientCreateRequest; res: AdminAbuseReportNotificationRecipientCreateResponse };
'admin/abuse-report/notification-recipient/update': { req: AdminAbuseReportNotificationRecipientUpdateRequest; res: AdminAbuseReportNotificationRecipientUpdateResponse };
- 'admin/abuse-report/notification-recipient/delete': { req: AdminAbuseReportNotificationRecipientDeleteRequest; res: EmptyResponse };
+ 'admin/abuse-user-reports': { req: AdminAbuseUserReportsRequest; res: AdminAbuseUserReportsResponse };
'admin/accounts/create': { req: AdminAccountsCreateRequest; res: AdminAccountsCreateResponse };
'admin/accounts/delete': { req: AdminAccountsDeleteRequest; res: EmptyResponse };
'admin/accounts/find-by-email': { req: AdminAccountsFindByEmailRequest; res: AdminAccountsFindByEmailResponse };
@@ -623,25 +628,29 @@ export type Endpoints = {
'admin/announcements/delete': { req: AdminAnnouncementsDeleteRequest; res: EmptyResponse };
'admin/announcements/list': { req: AdminAnnouncementsListRequest; res: AdminAnnouncementsListResponse };
'admin/announcements/update': { req: AdminAnnouncementsUpdateRequest; res: EmptyResponse };
+ 'admin/approve-user': { req: AdminApproveUserRequest; res: EmptyResponse };
'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: AdminAvatarDecorationsCreateResponse };
'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse };
'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse };
'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
+ 'admin/captcha/current': { req: EmptyRequest; res: AdminCaptchaCurrentResponse };
+ 'admin/captcha/save': { req: AdminCaptchaSaveRequest; res: EmptyResponse };
+ 'admin/cw-user': { req: AdminCwUserRequest; res: EmptyResponse };
+ 'admin/decline-user': { req: AdminDeclineUserRequest; res: EmptyResponse };
+ 'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse };
'admin/delete-all-files-of-a-user': { req: AdminDeleteAllFilesOfAUserRequest; res: EmptyResponse };
- 'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse };
- 'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse };
'admin/drive/clean-remote-files': { req: EmptyRequest; res: EmptyResponse };
'admin/drive/cleanup': { req: EmptyRequest; res: EmptyResponse };
'admin/drive/files': { req: AdminDriveFilesRequest; res: AdminDriveFilesResponse };
'admin/drive/show-file': { req: AdminDriveShowFileRequest; res: AdminDriveShowFileResponse };
- 'admin/emoji/add-aliases-bulk': { req: AdminEmojiAddAliasesBulkRequest; res: EmptyResponse };
'admin/emoji/add': { req: AdminEmojiAddRequest; res: AdminEmojiAddResponse };
+ 'admin/emoji/add-aliases-bulk': { req: AdminEmojiAddAliasesBulkRequest; res: EmptyResponse };
'admin/emoji/copy': { req: AdminEmojiCopyRequest; res: AdminEmojiCopyResponse };
- 'admin/emoji/delete-bulk': { req: AdminEmojiDeleteBulkRequest; res: EmptyResponse };
'admin/emoji/delete': { req: AdminEmojiDeleteRequest; res: EmptyResponse };
+ 'admin/emoji/delete-bulk': { req: AdminEmojiDeleteBulkRequest; res: EmptyResponse };
'admin/emoji/import-zip': { req: AdminEmojiImportZipRequest; res: EmptyResponse };
- 'admin/emoji/list-remote': { req: AdminEmojiListRemoteRequest; res: AdminEmojiListRemoteResponse };
'admin/emoji/list': { req: AdminEmojiListRequest; res: AdminEmojiListResponse };
+ 'admin/emoji/list-remote': { req: AdminEmojiListRemoteRequest; res: AdminEmojiListRemoteResponse };
'admin/emoji/remove-aliases-bulk': { req: AdminEmojiRemoveAliasesBulkRequest; res: EmptyResponse };
'admin/emoji/set-aliases-bulk': { req: AdminEmojiSetAliasesBulkRequest; res: EmptyResponse };
'admin/emoji/set-category-bulk': { req: AdminEmojiSetCategoryBulkRequest; res: EmptyResponse };
@@ -651,55 +660,57 @@ export type Endpoints = {
'admin/federation/refresh-remote-instance-metadata': { req: AdminFederationRefreshRemoteInstanceMetadataRequest; res: EmptyResponse };
'admin/federation/remove-all-following': { req: AdminFederationRemoveAllFollowingRequest; res: EmptyResponse };
'admin/federation/update-instance': { req: AdminFederationUpdateInstanceRequest; res: EmptyResponse };
+ 'admin/forward-abuse-user-report': { req: AdminForwardAbuseUserReportRequest; res: EmptyResponse };
+ 'admin/gen-vapid-keys': { req: EmptyRequest; res: EmptyResponse };
'admin/get-index-stats': { req: EmptyRequest; res: AdminGetIndexStatsResponse };
'admin/get-table-stats': { req: EmptyRequest; res: AdminGetTableStatsResponse };
'admin/get-user-ips': { req: AdminGetUserIpsRequest; res: AdminGetUserIpsResponse };
'admin/invite/create': { req: AdminInviteCreateRequest; res: AdminInviteCreateResponse };
'admin/invite/list': { req: AdminInviteListRequest; res: AdminInviteListResponse };
+ 'admin/meta': { req: EmptyRequest; res: AdminMetaResponse };
+ 'admin/nsfw-user': { req: AdminNsfwUserRequest; res: EmptyResponse };
'admin/promo/create': { req: AdminPromoCreateRequest; res: EmptyResponse };
'admin/queue/clear': { req: EmptyRequest; res: EmptyResponse };
'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse };
'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse };
'admin/queue/promote': { req: AdminQueuePromoteRequest; res: EmptyResponse };
'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse };
+ 'admin/reject-quotes': { req: AdminRejectQuotesRequest; res: EmptyResponse };
'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse };
'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse };
'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse };
'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse };
- 'admin/forward-abuse-user-report': { req: AdminForwardAbuseUserReportRequest; res: EmptyResponse };
- 'admin/update-abuse-user-report': { req: AdminUpdateAbuseUserReportRequest; res: EmptyResponse };
- 'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
- 'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
- 'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
- 'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse };
- 'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse };
- 'admin/nsfw-user': { req: AdminNsfwUserRequest; res: EmptyResponse };
- 'admin/unnsfw-user': { req: AdminUnnsfwUserRequest; res: EmptyResponse };
- 'admin/silence-user': { req: AdminSilenceUserRequest; res: EmptyResponse };
- 'admin/unsilence-user': { req: AdminUnsilenceUserRequest; res: EmptyResponse };
- 'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse };
- 'admin/approve-user': { req: AdminApproveUserRequest; res: EmptyResponse };
- 'admin/decline-user': { req: AdminDeclineUserRequest; res: EmptyResponse };
- 'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse };
- 'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse };
- 'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse };
- 'admin/update-user-note': { req: AdminUpdateUserNoteRequest; res: EmptyResponse };
+ 'admin/roles/assign': { req: AdminRolesAssignRequest; res: EmptyResponse };
'admin/roles/create': { req: AdminRolesCreateRequest; res: AdminRolesCreateResponse };
'admin/roles/delete': { req: AdminRolesDeleteRequest; res: EmptyResponse };
'admin/roles/list': { req: EmptyRequest; res: AdminRolesListResponse };
'admin/roles/show': { req: AdminRolesShowRequest; res: AdminRolesShowResponse };
- 'admin/roles/update': { req: AdminRolesUpdateRequest; res: EmptyResponse };
- 'admin/roles/assign': { req: AdminRolesAssignRequest; res: EmptyResponse };
'admin/roles/unassign': { req: AdminRolesUnassignRequest; res: EmptyResponse };
+ 'admin/roles/update': { req: AdminRolesUpdateRequest; res: EmptyResponse };
'admin/roles/update-default-policies': { req: AdminRolesUpdateDefaultPoliciesRequest; res: EmptyResponse };
'admin/roles/users': { req: AdminRolesUsersRequest; res: AdminRolesUsersResponse };
+ 'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
+ 'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
+ 'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
+ 'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse };
+ 'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse };
+ 'admin/silence-user': { req: AdminSilenceUserRequest; res: EmptyResponse };
+ 'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse };
'admin/system-webhook/create': { req: AdminSystemWebhookCreateRequest; res: AdminSystemWebhookCreateResponse };
'admin/system-webhook/delete': { req: AdminSystemWebhookDeleteRequest; res: EmptyResponse };
'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse };
'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
- 'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
'admin/system-webhook/test': { req: AdminSystemWebhookTestRequest; res: EmptyResponse };
+ 'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
+ 'admin/unnsfw-user': { req: AdminUnnsfwUserRequest; res: EmptyResponse };
+ 'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse };
+ 'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse };
+ 'admin/unsilence-user': { req: AdminUnsilenceUserRequest; res: EmptyResponse };
+ 'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse };
+ 'admin/update-abuse-user-report': { req: AdminUpdateAbuseUserReportRequest; res: EmptyResponse };
+ 'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse };
+ 'admin/update-user-note': { req: AdminUpdateUserNoteRequest; res: EmptyResponse };
'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
@@ -719,19 +730,21 @@ export type Endpoints = {
'blocking/create': { req: BlockingCreateRequest; res: BlockingCreateResponse };
'blocking/delete': { req: BlockingDeleteRequest; res: BlockingDeleteResponse };
'blocking/list': { req: BlockingListRequest; res: BlockingListResponse };
+ 'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse };
+ 'bubble-game/register': { req: BubbleGameRegisterRequest; res: EmptyResponse };
'channels/create': { req: ChannelsCreateRequest; res: ChannelsCreateResponse };
+ 'channels/favorite': { req: ChannelsFavoriteRequest; res: EmptyResponse };
'channels/featured': { req: EmptyRequest; res: ChannelsFeaturedResponse };
'channels/follow': { req: ChannelsFollowRequest; res: EmptyResponse };
'channels/followed': { req: ChannelsFollowedRequest; res: ChannelsFollowedResponse };
+ 'channels/my-favorites': { req: EmptyRequest; res: ChannelsMyFavoritesResponse };
'channels/owned': { req: ChannelsOwnedRequest; res: ChannelsOwnedResponse };
+ 'channels/search': { req: ChannelsSearchRequest; res: ChannelsSearchResponse };
'channels/show': { req: ChannelsShowRequest; res: ChannelsShowResponse };
'channels/timeline': { req: ChannelsTimelineRequest; res: ChannelsTimelineResponse };
+ 'channels/unfavorite': { req: ChannelsUnfavoriteRequest; res: EmptyResponse };
'channels/unfollow': { req: ChannelsUnfollowRequest; res: EmptyResponse };
'channels/update': { req: ChannelsUpdateRequest; res: ChannelsUpdateResponse };
- 'channels/favorite': { req: ChannelsFavoriteRequest; res: EmptyResponse };
- 'channels/unfavorite': { req: ChannelsUnfavoriteRequest; res: EmptyResponse };
- 'channels/my-favorites': { req: EmptyRequest; res: ChannelsMyFavoritesResponse };
- 'channels/search': { req: ChannelsSearchRequest; res: ChannelsSearchResponse };
'charts/active-users': { req: ChartsActiveUsersRequest; res: ChartsActiveUsersResponse };
'charts/ap-request': { req: ChartsApRequestRequest; res: ChartsApRequestResponse };
'charts/drive': { req: ChartsDriveRequest; res: ChartsDriveResponse };
@@ -745,24 +758,24 @@ export type Endpoints = {
'charts/user/reactions': { req: ChartsUserReactionsRequest; res: ChartsUserReactionsResponse };
'charts/users': { req: ChartsUsersRequest; res: ChartsUsersResponse };
'clips/add-note': { req: ClipsAddNoteRequest; res: EmptyResponse };
- 'clips/remove-note': { req: ClipsRemoveNoteRequest; res: EmptyResponse };
'clips/create': { req: ClipsCreateRequest; res: ClipsCreateResponse };
'clips/delete': { req: ClipsDeleteRequest; res: EmptyResponse };
+ 'clips/favorite': { req: ClipsFavoriteRequest; res: EmptyResponse };
'clips/list': { req: EmptyRequest; res: ClipsListResponse };
+ 'clips/my-favorites': { req: EmptyRequest; res: ClipsMyFavoritesResponse };
'clips/notes': { req: ClipsNotesRequest; res: ClipsNotesResponse };
+ 'clips/remove-note': { req: ClipsRemoveNoteRequest; res: EmptyResponse };
'clips/show': { req: ClipsShowRequest; res: ClipsShowResponse };
- 'clips/update': { req: ClipsUpdateRequest; res: ClipsUpdateResponse };
- 'clips/favorite': { req: ClipsFavoriteRequest; res: EmptyResponse };
'clips/unfavorite': { req: ClipsUnfavoriteRequest; res: EmptyResponse };
- 'clips/my-favorites': { req: EmptyRequest; res: ClipsMyFavoritesResponse };
+ 'clips/update': { req: ClipsUpdateRequest; res: ClipsUpdateResponse };
'drive': { req: EmptyRequest; res: DriveResponse };
'drive/files': { req: DriveFilesRequest; res: DriveFilesResponse };
'drive/files/attached-notes': { req: DriveFilesAttachedNotesRequest; res: DriveFilesAttachedNotesResponse };
'drive/files/check-existence': { req: DriveFilesCheckExistenceRequest; res: DriveFilesCheckExistenceResponse };
'drive/files/create': { req: DriveFilesCreateRequest; res: DriveFilesCreateResponse };
'drive/files/delete': { req: DriveFilesDeleteRequest; res: EmptyResponse };
- 'drive/files/find-by-hash': { req: DriveFilesFindByHashRequest; res: DriveFilesFindByHashResponse };
'drive/files/find': { req: DriveFilesFindRequest; res: DriveFilesFindResponse };
+ 'drive/files/find-by-hash': { req: DriveFilesFindByHashRequest; res: DriveFilesFindByHashResponse };
'drive/files/show': { req: DriveFilesShowRequest; res: DriveFilesShowResponse };
'drive/files/update': { req: DriveFilesUpdateRequest; res: DriveFilesUpdateResponse };
'drive/files/upload-from-url': { req: DriveFilesUploadFromUrlRequest; res: EmptyResponse };
@@ -774,6 +787,8 @@ export type Endpoints = {
'drive/folders/update': { req: DriveFoldersUpdateRequest; res: DriveFoldersUpdateResponse };
'drive/stream': { req: DriveStreamRequest; res: DriveStreamResponse };
'email-address/available': { req: EmailAddressAvailableRequest; res: EmailAddressAvailableResponse };
+ 'emoji': { req: EmojiRequest; res: EmojiResponse };
+ 'emojis': { req: EmptyRequest; res: EmojisResponse };
'endpoint': { req: EndpointRequest; res: EndpointResponse };
'endpoints': { req: EmptyRequest; res: EndpointsResponse };
'export-custom-emojis': { req: EmptyRequest; res: EmptyResponse };
@@ -781,19 +796,30 @@ export type Endpoints = {
'federation/following': { req: FederationFollowingRequest; res: FederationFollowingResponse };
'federation/instances': { req: FederationInstancesRequest; res: FederationInstancesResponse };
'federation/show-instance': { req: FederationShowInstanceRequest; res: FederationShowInstanceResponse };
+ 'federation/stats': { req: FederationStatsRequest; res: FederationStatsResponse };
'federation/update-remote-user': { req: FederationUpdateRemoteUserRequest; res: EmptyResponse };
'federation/users': { req: FederationUsersRequest; res: FederationUsersResponse };
- 'federation/stats': { req: FederationStatsRequest; res: FederationStatsResponse };
+ 'fetch-external-resources': { req: FetchExternalResourcesRequest; res: FetchExternalResourcesResponse };
+ 'fetch-rss': { req: FetchRssRequest; res: FetchRssResponse };
+ 'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
+ 'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
+ 'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
+ 'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
+ 'flash/my': { req: FlashMyRequest; res: FlashMyResponse };
+ 'flash/my-likes': { req: FlashMyLikesRequest; res: FlashMyLikesResponse };
+ 'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
+ 'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
+ 'flash/update': { req: FlashUpdateRequest; res: EmptyResponse };
'following/create': { req: FollowingCreateRequest; res: FollowingCreateResponse };
'following/delete': { req: FollowingDeleteRequest; res: FollowingDeleteResponse };
- 'following/update': { req: FollowingUpdateRequest; res: FollowingUpdateResponse };
- 'following/update-all': { req: FollowingUpdateAllRequest; res: EmptyResponse };
'following/invalidate': { req: FollowingInvalidateRequest; res: FollowingInvalidateResponse };
'following/requests/accept': { req: FollowingRequestsAcceptRequest; res: EmptyResponse };
'following/requests/cancel': { req: FollowingRequestsCancelRequest; res: FollowingRequestsCancelResponse };
'following/requests/list': { req: FollowingRequestsListRequest; res: FollowingRequestsListResponse };
- 'following/requests/sent': { req: FollowingRequestsSentRequest; res: FollowingRequestsSentResponse };
'following/requests/reject': { req: FollowingRequestsRejectRequest; res: EmptyResponse };
+ 'following/requests/sent': { req: FollowingRequestsSentRequest; res: FollowingRequestsSentResponse };
+ 'following/update': { req: FollowingUpdateRequest; res: FollowingUpdateResponse };
+ 'following/update-all': { req: FollowingUpdateAllRequest; res: EmptyResponse };
'gallery/featured': { req: GalleryFeaturedRequest; res: GalleryFeaturedResponse };
'gallery/popular': { req: EmptyRequest; res: GalleryPopularResponse };
'gallery/posts': { req: GalleryPostsRequest; res: GalleryPostsResponse };
@@ -803,8 +829,8 @@ export type Endpoints = {
'gallery/posts/show': { req: GalleryPostsShowRequest; res: GalleryPostsShowResponse };
'gallery/posts/unlike': { req: GalleryPostsUnlikeRequest; res: EmptyResponse };
'gallery/posts/update': { req: GalleryPostsUpdateRequest; res: GalleryPostsUpdateResponse };
- 'get-online-users-count': { req: EmptyRequest; res: GetOnlineUsersCountResponse };
'get-avatar-decorations': { req: EmptyRequest; res: GetAvatarDecorationsResponse };
+ 'get-online-users-count': { req: EmptyRequest; res: GetOnlineUsersCountResponse };
'hashtags/list': { req: HashtagsListRequest; res: HashtagsListResponse };
'hashtags/search': { req: HashtagsSearchRequest; res: HashtagsSearchResponse };
'hashtags/show': { req: HashtagsShowRequest; res: HashtagsShowResponse };
@@ -814,34 +840,35 @@ export type Endpoints = {
'i/2fa/done': { req: I2faDoneRequest; res: I2faDoneResponse };
'i/2fa/key-done': { req: I2faKeyDoneRequest; res: I2faKeyDoneResponse };
'i/2fa/password-less': { req: I2faPasswordLessRequest; res: EmptyResponse };
- 'i/2fa/register-key': { req: I2faRegisterKeyRequest; res: I2faRegisterKeyResponse };
'i/2fa/register': { req: I2faRegisterRequest; res: I2faRegisterResponse };
- 'i/2fa/update-key': { req: I2faUpdateKeyRequest; res: EmptyResponse };
+ 'i/2fa/register-key': { req: I2faRegisterKeyRequest; res: I2faRegisterKeyResponse };
'i/2fa/remove-key': { req: I2faRemoveKeyRequest; res: EmptyResponse };
'i/2fa/unregister': { req: I2faUnregisterRequest; res: EmptyResponse };
+ 'i/2fa/update-key': { req: I2faUpdateKeyRequest; res: EmptyResponse };
'i/apps': { req: IAppsRequest; res: IAppsResponse };
'i/authorized-apps': { req: IAuthorizedAppsRequest; res: IAuthorizedAppsResponse };
- 'i/claim-achievement': { req: IClaimAchievementRequest; res: EmptyResponse };
'i/change-password': { req: IChangePasswordRequest; res: EmptyResponse };
+ 'i/claim-achievement': { req: IClaimAchievementRequest; res: EmptyResponse };
'i/delete-account': { req: IDeleteAccountRequest; res: EmptyResponse };
- 'i/export-data': { req: EmptyRequest; res: EmptyResponse };
+ 'i/export-antennas': { req: EmptyRequest; res: EmptyResponse };
'i/export-blocking': { req: EmptyRequest; res: EmptyResponse };
+ 'i/export-clips': { req: EmptyRequest; res: EmptyResponse };
+ 'i/export-data': { req: EmptyRequest; res: EmptyResponse };
+ 'i/export-favorites': { req: EmptyRequest; res: EmptyResponse };
'i/export-following': { req: IExportFollowingRequest; res: EmptyResponse };
'i/export-mute': { req: EmptyRequest; res: EmptyResponse };
'i/export-notes': { req: EmptyRequest; res: EmptyResponse };
- 'i/export-clips': { req: EmptyRequest; res: EmptyResponse };
- 'i/export-favorites': { req: EmptyRequest; res: EmptyResponse };
'i/export-user-lists': { req: EmptyRequest; res: EmptyResponse };
- 'i/export-antennas': { req: EmptyRequest; res: EmptyResponse };
'i/favorites': { req: IFavoritesRequest; res: IFavoritesResponse };
'i/gallery/likes': { req: IGalleryLikesRequest; res: IGalleryLikesResponse };
'i/gallery/posts': { req: IGalleryPostsRequest; res: IGalleryPostsResponse };
+ 'i/import-antennas': { req: IImportAntennasRequest; res: EmptyResponse };
'i/import-blocking': { req: IImportBlockingRequest; res: EmptyResponse };
'i/import-following': { req: IImportFollowingRequest; res: EmptyResponse };
- 'i/import-notes': { req: IImportNotesRequest; res: EmptyResponse };
'i/import-muting': { req: IImportMutingRequest; res: EmptyResponse };
+ 'i/import-notes': { req: IImportNotesRequest; res: EmptyResponse };
'i/import-user-lists': { req: IImportUserListsRequest; res: EmptyResponse };
- 'i/import-antennas': { req: IImportAntennasRequest; res: EmptyResponse };
+ 'i/move': { req: IMoveRequest; res: IMoveResponse };
'i/notifications': { req: INotificationsRequest; res: INotificationsResponse };
'i/notifications-grouped': { req: INotificationsGroupedRequest; res: INotificationsGroupedResponse };
'i/page-likes': { req: IPageLikesRequest; res: IPageLikesResponse };
@@ -850,71 +877,66 @@ export type Endpoints = {
'i/read-all-unread-notes': { req: EmptyRequest; res: EmptyResponse };
'i/read-announcement': { req: IReadAnnouncementRequest; res: EmptyResponse };
'i/regenerate-token': { req: IRegenerateTokenRequest; res: EmptyResponse };
+ 'i/registry/get': { req: IRegistryGetRequest; res: IRegistryGetResponse };
'i/registry/get-all': { req: IRegistryGetAllRequest; res: IRegistryGetAllResponse };
- 'i/registry/get-unsecure': { req: IRegistryGetUnsecureRequest; res: EmptyResponse };
'i/registry/get-detail': { req: IRegistryGetDetailRequest; res: IRegistryGetDetailResponse };
- 'i/registry/get': { req: IRegistryGetRequest; res: IRegistryGetResponse };
- 'i/registry/keys-with-type': { req: IRegistryKeysWithTypeRequest; res: IRegistryKeysWithTypeResponse };
+ 'i/registry/get-unsecure': { req: IRegistryGetUnsecureRequest; res: EmptyResponse };
'i/registry/keys': { req: IRegistryKeysRequest; res: IRegistryKeysResponse };
+ 'i/registry/keys-with-type': { req: IRegistryKeysWithTypeRequest; res: IRegistryKeysWithTypeResponse };
'i/registry/remove': { req: IRegistryRemoveRequest; res: EmptyResponse };
'i/registry/scopes-with-domain': { req: EmptyRequest; res: IRegistryScopesWithDomainResponse };
'i/registry/set': { req: IRegistrySetRequest; res: EmptyResponse };
'i/revoke-token': { req: IRevokeTokenRequest; res: EmptyResponse };
'i/signin-history': { req: ISigninHistoryRequest; res: ISigninHistoryResponse };
'i/unpin': { req: IUnpinRequest; res: IUnpinResponse };
- 'i/update-email': { req: IUpdateEmailRequest; res: IUpdateEmailResponse };
'i/update': { req: IUpdateRequest; res: IUpdateResponse };
- 'i/move': { req: IMoveRequest; res: IMoveResponse };
+ 'i/update-email': { req: IUpdateEmailRequest; res: IUpdateEmailResponse };
'i/webhooks/create': { req: IWebhooksCreateRequest; res: IWebhooksCreateResponse };
+ 'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse };
'i/webhooks/list': { req: EmptyRequest; res: IWebhooksListResponse };
'i/webhooks/show': { req: IWebhooksShowRequest; res: IWebhooksShowResponse };
- 'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse };
- 'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse };
'i/webhooks/test': { req: IWebhooksTestRequest; res: EmptyResponse };
+ 'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse };
'invite/create': { req: EmptyRequest; res: InviteCreateResponse };
'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse };
- 'invite/list': { req: InviteListRequest; res: InviteListResponse };
'invite/limit': { req: EmptyRequest; res: InviteLimitResponse };
+ 'invite/list': { req: InviteListRequest; res: InviteListResponse };
'meta': { req: MetaRequest; res: MetaResponse };
- 'emojis': { req: EmptyRequest; res: EmojisResponse };
- 'emoji': { req: EmojiRequest; res: EmojiResponse };
'miauth/gen-token': { req: MiauthGenTokenRequest; res: MiauthGenTokenResponse };
'mute/create': { req: MuteCreateRequest; res: EmptyResponse };
'mute/delete': { req: MuteDeleteRequest; res: EmptyResponse };
'mute/list': { req: MuteListRequest; res: MuteListResponse };
- 'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse };
- 'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse };
- 'renote-mute/list': { req: RenoteMuteListRequest; res: RenoteMuteListResponse };
'my/apps': { req: MyAppsRequest; res: MyAppsResponse };
'notes': { req: NotesRequest; res: NotesResponse };
+ 'notes/bubble-timeline': { req: NotesBubbleTimelineRequest; res: NotesBubbleTimelineResponse };
'notes/children': { req: NotesChildrenRequest; res: NotesChildrenResponse };
'notes/clips': { req: NotesClipsRequest; res: NotesClipsResponse };
'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse };
'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse };
'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse };
+ 'notes/edit': { req: NotesEditRequest; res: NotesEditResponse };
'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse };
'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse };
'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse };
'notes/following': { req: NotesFollowingRequest; res: NotesFollowingResponse };
'notes/global-timeline': { req: NotesGlobalTimelineRequest; res: NotesGlobalTimelineResponse };
- 'notes/bubble-timeline': { req: NotesBubbleTimelineRequest; res: NotesBubbleTimelineResponse };
'notes/hybrid-timeline': { req: NotesHybridTimelineRequest; res: NotesHybridTimelineResponse };
+ 'notes/like': { req: NotesLikeRequest; res: EmptyResponse };
'notes/local-timeline': { req: NotesLocalTimelineRequest; res: NotesLocalTimelineResponse };
'notes/mentions': { req: NotesMentionsRequest; res: NotesMentionsResponse };
'notes/polls/recommendation': { req: NotesPollsRecommendationRequest; res: NotesPollsRecommendationResponse };
- 'notes/polls/vote': { req: NotesPollsVoteRequest; res: EmptyResponse };
'notes/polls/refresh': { req: NotesPollsRefreshRequest; res: EmptyResponse };
+ 'notes/polls/vote': { req: NotesPollsVoteRequest; res: EmptyResponse };
'notes/reactions': { req: NotesReactionsRequest; res: NotesReactionsResponse };
'notes/reactions/create': { req: NotesReactionsCreateRequest; res: EmptyResponse };
'notes/reactions/delete': { req: NotesReactionsDeleteRequest; res: EmptyResponse };
- 'notes/like': { req: NotesLikeRequest; res: EmptyResponse };
'notes/renotes': { req: NotesRenotesRequest; res: NotesRenotesResponse };
'notes/replies': { req: NotesRepliesRequest; res: NotesRepliesResponse };
'notes/schedule/create': { req: NotesScheduleCreateRequest; res: EmptyResponse };
'notes/schedule/delete': { req: NotesScheduleDeleteRequest; res: EmptyResponse };
'notes/schedule/list': { req: NotesScheduleListRequest; res: NotesScheduleListResponse };
- 'notes/search-by-tag': { req: NotesSearchByTagRequest; res: NotesSearchByTagResponse };
'notes/search': { req: NotesSearchRequest; res: NotesSearchResponse };
+ 'notes/search-by-tag': { req: NotesSearchByTagRequest; res: NotesSearchByTagResponse };
'notes/show': { req: NotesShowRequest; res: NotesShowResponse };
'notes/state': { req: NotesStateRequest; res: NotesStateResponse };
'notes/thread-muting/create': { req: NotesThreadMutingCreateRequest; res: EmptyResponse };
@@ -923,7 +945,6 @@ export type Endpoints = {
'notes/translate': { req: NotesTranslateRequest; res: NotesTranslateResponse };
'notes/unrenote': { req: NotesUnrenoteRequest; res: EmptyResponse };
'notes/user-list-timeline': { req: NotesUserListTimelineRequest; res: NotesUserListTimelineResponse };
- 'notes/edit': { req: NotesEditRequest; res: NotesEditResponse };
'notes/versions': { req: NotesVersionsRequest; res: NotesVersionsResponse };
'notifications/create': { req: NotificationsCreateRequest; res: EmptyResponse };
'notifications/flush': { req: EmptyRequest; res: EmptyResponse };
@@ -937,77 +958,68 @@ export type Endpoints = {
'pages/show': { req: PagesShowRequest; res: PagesShowResponse };
'pages/unlike': { req: PagesUnlikeRequest; res: EmptyResponse };
'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
- 'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
- 'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
- 'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
- 'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
- 'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
- 'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
- 'flash/update': { req: FlashUpdateRequest; res: EmptyResponse };
- 'flash/my': { req: FlashMyRequest; res: FlashMyResponse };
- 'flash/my-likes': { req: FlashMyLikesRequest; res: FlashMyLikesResponse };
'ping': { req: EmptyRequest; res: PingResponse };
'pinned-users': { req: EmptyRequest; res: PinnedUsersResponse };
'promo/read': { req: PromoReadRequest; res: EmptyResponse };
- 'roles/list': { req: EmptyRequest; res: RolesListResponse };
- 'roles/show': { req: RolesShowRequest; res: RolesShowResponse };
- 'roles/users': { req: RolesUsersRequest; res: RolesUsersResponse };
- 'roles/notes': { req: RolesNotesRequest; res: RolesNotesResponse };
+ 'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse };
+ 'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse };
+ 'renote-mute/list': { req: RenoteMuteListRequest; res: RenoteMuteListResponse };
'request-reset-password': { req: RequestResetPasswordRequest; res: EmptyResponse };
'reset-db': { req: EmptyRequest; res: EmptyResponse };
'reset-password': { req: ResetPasswordRequest; res: EmptyResponse };
+ 'retention': { req: EmptyRequest; res: RetentionResponse };
+ 'reversi/cancel-match': { req: ReversiCancelMatchRequest; res: EmptyResponse };
+ 'reversi/games': { req: ReversiGamesRequest; res: ReversiGamesResponse };
+ 'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse };
+ 'reversi/match': { req: ReversiMatchRequest; res: ReversiMatchResponse };
+ 'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse };
+ 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse };
+ 'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse };
+ 'roles/list': { req: EmptyRequest; res: RolesListResponse };
+ 'roles/notes': { req: RolesNotesRequest; res: RolesNotesResponse };
+ 'roles/show': { req: RolesShowRequest; res: RolesShowResponse };
+ 'roles/users': { req: RolesUsersRequest; res: RolesUsersResponse };
'server-info': { req: EmptyRequest; res: ServerInfoResponse };
+ 'sponsors': { req: SponsorsRequest; res: EmptyResponse };
'stats': { req: EmptyRequest; res: StatsResponse };
- 'sw/show-registration': { req: SwShowRegistrationRequest; res: SwShowRegistrationResponse };
- 'sw/update-registration': { req: SwUpdateRegistrationRequest; res: SwUpdateRegistrationResponse };
'sw/register': { req: SwRegisterRequest; res: SwRegisterResponse };
+ 'sw/show-registration': { req: SwShowRegistrationRequest; res: SwShowRegistrationResponse };
'sw/unregister': { req: SwUnregisterRequest; res: EmptyResponse };
+ 'sw/update-registration': { req: SwUpdateRegistrationRequest; res: SwUpdateRegistrationResponse };
'test': { req: TestRequest; res: TestResponse };
'username/available': { req: UsernameAvailableRequest; res: UsernameAvailableResponse };
'users': { req: UsersRequest; res: UsersResponse };
+ 'users/achievements': { req: UsersAchievementsRequest; res: UsersAchievementsResponse };
'users/clips': { req: UsersClipsRequest; res: UsersClipsResponse };
+ 'users/featured-notes': { req: UsersFeaturedNotesRequest; res: UsersFeaturedNotesResponse };
+ 'users/flashs': { req: UsersFlashsRequest; res: UsersFlashsResponse };
'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse };
'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse };
'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse };
'users/get-frequently-replied-users': { req: UsersGetFrequentlyRepliedUsersRequest; res: UsersGetFrequentlyRepliedUsersResponse };
- 'users/featured-notes': { req: UsersFeaturedNotesRequest; res: UsersFeaturedNotesResponse };
'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse };
+ 'users/lists/create-from-public': { req: UsersListsCreateFromPublicRequest; res: UsersListsCreateFromPublicResponse };
'users/lists/delete': { req: UsersListsDeleteRequest; res: EmptyResponse };
+ 'users/lists/favorite': { req: UsersListsFavoriteRequest; res: EmptyResponse };
+ 'users/lists/get-memberships': { req: UsersListsGetMembershipsRequest; res: UsersListsGetMembershipsResponse };
'users/lists/list': { req: UsersListsListRequest; res: UsersListsListResponse };
'users/lists/pull': { req: UsersListsPullRequest; res: EmptyResponse };
'users/lists/push': { req: UsersListsPushRequest; res: EmptyResponse };
'users/lists/show': { req: UsersListsShowRequest; res: UsersListsShowResponse };
- 'users/lists/favorite': { req: UsersListsFavoriteRequest; res: EmptyResponse };
'users/lists/unfavorite': { req: UsersListsUnfavoriteRequest; res: EmptyResponse };
'users/lists/update': { req: UsersListsUpdateRequest; res: UsersListsUpdateResponse };
- 'users/lists/create-from-public': { req: UsersListsCreateFromPublicRequest; res: UsersListsCreateFromPublicResponse };
'users/lists/update-membership': { req: UsersListsUpdateMembershipRequest; res: EmptyResponse };
- 'users/lists/get-memberships': { req: UsersListsGetMembershipsRequest; res: UsersListsGetMembershipsResponse };
'users/notes': { req: UsersNotesRequest; res: UsersNotesResponse };
'users/pages': { req: UsersPagesRequest; res: UsersPagesResponse };
- 'users/flashs': { req: UsersFlashsRequest; res: UsersFlashsResponse };
'users/reactions': { req: UsersReactionsRequest; res: UsersReactionsResponse };
'users/recommendation': { req: UsersRecommendationRequest; res: UsersRecommendationResponse };
'users/relation': { req: UsersRelationRequest; res: UsersRelationResponse };
'users/report-abuse': { req: UsersReportAbuseRequest; res: EmptyResponse };
- 'users/search-by-username-and-host': { req: UsersSearchByUsernameAndHostRequest; res: UsersSearchByUsernameAndHostResponse };
'users/search': { req: UsersSearchRequest; res: UsersSearchResponse };
+ 'users/search-by-username-and-host': { req: UsersSearchByUsernameAndHostRequest; res: UsersSearchByUsernameAndHostResponse };
'users/show': { req: UsersShowRequest; res: UsersShowResponse };
- 'users/achievements': { req: UsersAchievementsRequest; res: UsersAchievementsResponse };
'users/update-memo': { req: UsersUpdateMemoRequest; res: EmptyResponse };
- 'fetch-rss': { req: FetchRssRequest; res: FetchRssResponse };
- 'fetch-external-resources': { req: FetchExternalResourcesRequest; res: FetchExternalResourcesResponse };
- 'retention': { req: EmptyRequest; res: RetentionResponse };
- 'sponsors': { req: SponsorsRequest; res: EmptyResponse };
- 'bubble-game/register': { req: BubbleGameRegisterRequest; res: EmptyResponse };
- 'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse };
- 'reversi/cancel-match': { req: ReversiCancelMatchRequest; res: EmptyResponse };
- 'reversi/games': { req: ReversiGamesRequest; res: ReversiGamesResponse };
- 'reversi/match': { req: ReversiMatchRequest; res: ReversiMatchResponse };
- 'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse };
- 'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse };
- 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse };
- 'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse };
+ 'v2/admin/emoji/list': { req: V2AdminEmojiListRequest; res: V2AdminEmojiListResponse };
}
/**
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 9166bb701f..df411153fd 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -4,18 +4,17 @@ import { operations } from './types.js';
export type EmptyRequest = Record<string, unknown> | undefined;
export type EmptyResponse = Record<string, unknown> | undefined;
-export type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json'];
-export type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json'];
-export type AdminAbuseUserReportsResponse = operations['admin___abuse-user-reports']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json'];
export type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json'];
export type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json'];
export type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json'];
export type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json'];
-export type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json'];
-export type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json'];
export type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json'];
export type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json'];
-export type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json'];
+export type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json'];
+export type AdminAbuseUserReportsResponse = operations['admin___abuse-user-reports']['responses']['200']['content']['application/json'];
export type AdminAccountsCreateRequest = operations['admin___accounts___create']['requestBody']['content']['application/json'];
export type AdminAccountsCreateResponse = operations['admin___accounts___create']['responses']['200']['content']['application/json'];
export type AdminAccountsDeleteRequest = operations['admin___accounts___delete']['requestBody']['content']['application/json'];
@@ -33,31 +32,35 @@ export type AdminAnnouncementsDeleteRequest = operations['admin___announcements_
export type AdminAnnouncementsListRequest = operations['admin___announcements___list']['requestBody']['content']['application/json'];
export type AdminAnnouncementsListResponse = operations['admin___announcements___list']['responses']['200']['content']['application/json'];
export type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json'];
+export type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json'];
export type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json'];
+export type AdminCaptchaCurrentResponse = operations['admin___captcha___current']['responses']['200']['content']['application/json'];
+export type AdminCaptchaSaveRequest = operations['admin___captcha___save']['requestBody']['content']['application/json'];
+export type AdminCwUserRequest = operations['admin___cw-user']['requestBody']['content']['application/json'];
+export type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json'];
+export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json'];
export type AdminDeleteAllFilesOfAUserRequest = operations['admin___delete-all-files-of-a-user']['requestBody']['content']['application/json'];
-export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
-export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json'];
export type AdminDriveFilesRequest = operations['admin___drive___files']['requestBody']['content']['application/json'];
export type AdminDriveFilesResponse = operations['admin___drive___files']['responses']['200']['content']['application/json'];
export type AdminDriveShowFileRequest = operations['admin___drive___show-file']['requestBody']['content']['application/json'];
export type AdminDriveShowFileResponse = operations['admin___drive___show-file']['responses']['200']['content']['application/json'];
-export type AdminEmojiAddAliasesBulkRequest = operations['admin___emoji___add-aliases-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiAddRequest = operations['admin___emoji___add']['requestBody']['content']['application/json'];
export type AdminEmojiAddResponse = operations['admin___emoji___add']['responses']['200']['content']['application/json'];
+export type AdminEmojiAddAliasesBulkRequest = operations['admin___emoji___add-aliases-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiCopyRequest = operations['admin___emoji___copy']['requestBody']['content']['application/json'];
export type AdminEmojiCopyResponse = operations['admin___emoji___copy']['responses']['200']['content']['application/json'];
-export type AdminEmojiDeleteBulkRequest = operations['admin___emoji___delete-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiDeleteRequest = operations['admin___emoji___delete']['requestBody']['content']['application/json'];
+export type AdminEmojiDeleteBulkRequest = operations['admin___emoji___delete-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiImportZipRequest = operations['admin___emoji___import-zip']['requestBody']['content']['application/json'];
-export type AdminEmojiListRemoteRequest = operations['admin___emoji___list-remote']['requestBody']['content']['application/json'];
-export type AdminEmojiListRemoteResponse = operations['admin___emoji___list-remote']['responses']['200']['content']['application/json'];
export type AdminEmojiListRequest = operations['admin___emoji___list']['requestBody']['content']['application/json'];
export type AdminEmojiListResponse = operations['admin___emoji___list']['responses']['200']['content']['application/json'];
+export type AdminEmojiListRemoteRequest = operations['admin___emoji___list-remote']['requestBody']['content']['application/json'];
+export type AdminEmojiListRemoteResponse = operations['admin___emoji___list-remote']['responses']['200']['content']['application/json'];
export type AdminEmojiRemoveAliasesBulkRequest = operations['admin___emoji___remove-aliases-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiSetAliasesBulkRequest = operations['admin___emoji___set-aliases-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiSetCategoryBulkRequest = operations['admin___emoji___set-category-bulk']['requestBody']['content']['application/json'];
@@ -67,6 +70,7 @@ export type AdminFederationDeleteAllFilesRequest = operations['admin___federatio
export type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['admin___federation___refresh-remote-instance-metadata']['requestBody']['content']['application/json'];
export type AdminFederationRemoveAllFollowingRequest = operations['admin___federation___remove-all-following']['requestBody']['content']['application/json'];
export type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json'];
+export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json'];
export type AdminGetTableStatsResponse = operations['admin___get-table-stats']['responses']['200']['content']['application/json'];
export type AdminGetUserIpsRequest = operations['admin___get-user-ips']['requestBody']['content']['application/json'];
@@ -75,11 +79,14 @@ export type AdminInviteCreateRequest = operations['admin___invite___create']['re
export type AdminInviteCreateResponse = operations['admin___invite___create']['responses']['200']['content']['application/json'];
export type AdminInviteListRequest = operations['admin___invite___list']['requestBody']['content']['application/json'];
export type AdminInviteListResponse = operations['admin___invite___list']['responses']['200']['content']['application/json'];
+export type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json'];
+export type AdminNsfwUserRequest = operations['admin___nsfw-user']['requestBody']['content']['application/json'];
export type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json'];
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
export type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBody']['content']['application/json'];
export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
+export type AdminRejectQuotesRequest = operations['admin___reject-quotes']['requestBody']['content']['application/json'];
export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json'];
export type AdminRelaysListResponse = operations['admin___relays___list']['responses']['200']['content']['application/json'];
@@ -87,39 +94,28 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re
export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json'];
export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json'];
export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
-export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
-export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
-export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json'];
-export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
-export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
-export type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json'];
-export type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json'];
-export type AdminShowUserResponse = operations['admin___show-user']['responses']['200']['content']['application/json'];
-export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json'];
-export type AdminShowUsersResponse = operations['admin___show-users']['responses']['200']['content']['application/json'];
-export type AdminNsfwUserRequest = operations['admin___nsfw-user']['requestBody']['content']['application/json'];
-export type AdminUnnsfwUserRequest = operations['admin___unnsfw-user']['requestBody']['content']['application/json'];
-export type AdminSilenceUserRequest = operations['admin___silence-user']['requestBody']['content']['application/json'];
-export type AdminUnsilenceUserRequest = operations['admin___unsilence-user']['requestBody']['content']['application/json'];
-export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json'];
-export type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json'];
-export type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json'];
-export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
-export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
-export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json'];
-export type AdminUpdateUserNoteRequest = operations['admin___update-user-note']['requestBody']['content']['application/json'];
+export type AdminRolesAssignRequest = operations['admin___roles___assign']['requestBody']['content']['application/json'];
export type AdminRolesCreateRequest = operations['admin___roles___create']['requestBody']['content']['application/json'];
export type AdminRolesCreateResponse = operations['admin___roles___create']['responses']['200']['content']['application/json'];
export type AdminRolesDeleteRequest = operations['admin___roles___delete']['requestBody']['content']['application/json'];
export type AdminRolesListResponse = operations['admin___roles___list']['responses']['200']['content']['application/json'];
export type AdminRolesShowRequest = operations['admin___roles___show']['requestBody']['content']['application/json'];
export type AdminRolesShowResponse = operations['admin___roles___show']['responses']['200']['content']['application/json'];
-export type AdminRolesUpdateRequest = operations['admin___roles___update']['requestBody']['content']['application/json'];
-export type AdminRolesAssignRequest = operations['admin___roles___assign']['requestBody']['content']['application/json'];
export type AdminRolesUnassignRequest = operations['admin___roles___unassign']['requestBody']['content']['application/json'];
+export type AdminRolesUpdateRequest = operations['admin___roles___update']['requestBody']['content']['application/json'];
export type AdminRolesUpdateDefaultPoliciesRequest = operations['admin___roles___update-default-policies']['requestBody']['content']['application/json'];
export type AdminRolesUsersRequest = operations['admin___roles___users']['requestBody']['content']['application/json'];
export type AdminRolesUsersResponse = operations['admin___roles___users']['responses']['200']['content']['application/json'];
+export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json'];
+export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
+export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
+export type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json'];
+export type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json'];
+export type AdminShowUserResponse = operations['admin___show-user']['responses']['200']['content']['application/json'];
+export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json'];
+export type AdminShowUsersResponse = operations['admin___show-users']['responses']['200']['content']['application/json'];
+export type AdminSilenceUserRequest = operations['admin___silence-user']['requestBody']['content']['application/json'];
+export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json'];
export type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json'];
export type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json'];
export type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json'];
@@ -127,9 +123,17 @@ export type AdminSystemWebhookListRequest = operations['admin___system-webhook__
export type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json'];
export type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json'];
export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
-export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
+export type AdminUnnsfwUserRequest = operations['admin___unnsfw-user']['requestBody']['content']['application/json'];
+export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
+export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json'];
+export type AdminUnsilenceUserRequest = operations['admin___unsilence-user']['requestBody']['content']['application/json'];
+export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
+export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
+export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
+export type AdminUpdateUserNoteRequest = operations['admin___update-user-note']['requestBody']['content']['application/json'];
export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json'];
export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json'];
export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json'];
@@ -165,26 +169,29 @@ export type BlockingDeleteRequest = operations['blocking___delete']['requestBody
export type BlockingDeleteResponse = operations['blocking___delete']['responses']['200']['content']['application/json'];
export type BlockingListRequest = operations['blocking___list']['requestBody']['content']['application/json'];
export type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json'];
+export type BubbleGameRankingRequest = operations['bubble-game___ranking']['requestBody']['content']['application/json'];
+export type BubbleGameRankingResponse = operations['bubble-game___ranking']['responses']['200']['content']['application/json'];
+export type BubbleGameRegisterRequest = operations['bubble-game___register']['requestBody']['content']['application/json'];
export type ChannelsCreateRequest = operations['channels___create']['requestBody']['content']['application/json'];
export type ChannelsCreateResponse = operations['channels___create']['responses']['200']['content']['application/json'];
+export type ChannelsFavoriteRequest = operations['channels___favorite']['requestBody']['content']['application/json'];
export type ChannelsFeaturedResponse = operations['channels___featured']['responses']['200']['content']['application/json'];
export type ChannelsFollowRequest = operations['channels___follow']['requestBody']['content']['application/json'];
export type ChannelsFollowedRequest = operations['channels___followed']['requestBody']['content']['application/json'];
export type ChannelsFollowedResponse = operations['channels___followed']['responses']['200']['content']['application/json'];
+export type ChannelsMyFavoritesResponse = operations['channels___my-favorites']['responses']['200']['content']['application/json'];
export type ChannelsOwnedRequest = operations['channels___owned']['requestBody']['content']['application/json'];
export type ChannelsOwnedResponse = operations['channels___owned']['responses']['200']['content']['application/json'];
+export type ChannelsSearchRequest = operations['channels___search']['requestBody']['content']['application/json'];
+export type ChannelsSearchResponse = operations['channels___search']['responses']['200']['content']['application/json'];
export type ChannelsShowRequest = operations['channels___show']['requestBody']['content']['application/json'];
export type ChannelsShowResponse = operations['channels___show']['responses']['200']['content']['application/json'];
export type ChannelsTimelineRequest = operations['channels___timeline']['requestBody']['content']['application/json'];
export type ChannelsTimelineResponse = operations['channels___timeline']['responses']['200']['content']['application/json'];
+export type ChannelsUnfavoriteRequest = operations['channels___unfavorite']['requestBody']['content']['application/json'];
export type ChannelsUnfollowRequest = operations['channels___unfollow']['requestBody']['content']['application/json'];
export type ChannelsUpdateRequest = operations['channels___update']['requestBody']['content']['application/json'];
export type ChannelsUpdateResponse = operations['channels___update']['responses']['200']['content']['application/json'];
-export type ChannelsFavoriteRequest = operations['channels___favorite']['requestBody']['content']['application/json'];
-export type ChannelsUnfavoriteRequest = operations['channels___unfavorite']['requestBody']['content']['application/json'];
-export type ChannelsMyFavoritesResponse = operations['channels___my-favorites']['responses']['200']['content']['application/json'];
-export type ChannelsSearchRequest = operations['channels___search']['requestBody']['content']['application/json'];
-export type ChannelsSearchResponse = operations['channels___search']['responses']['200']['content']['application/json'];
export type ChartsActiveUsersRequest = operations['charts___active-users']['requestBody']['content']['application/json'];
export type ChartsActiveUsersResponse = operations['charts___active-users']['responses']['200']['content']['application/json'];
export type ChartsApRequestRequest = operations['charts___ap-request']['requestBody']['content']['application/json'];
@@ -210,20 +217,20 @@ export type ChartsUserReactionsResponse = operations['charts___user___reactions'
export type ChartsUsersRequest = operations['charts___users']['requestBody']['content']['application/json'];
export type ChartsUsersResponse = operations['charts___users']['responses']['200']['content']['application/json'];
export type ClipsAddNoteRequest = operations['clips___add-note']['requestBody']['content']['application/json'];
-export type ClipsRemoveNoteRequest = operations['clips___remove-note']['requestBody']['content']['application/json'];
export type ClipsCreateRequest = operations['clips___create']['requestBody']['content']['application/json'];
export type ClipsCreateResponse = operations['clips___create']['responses']['200']['content']['application/json'];
export type ClipsDeleteRequest = operations['clips___delete']['requestBody']['content']['application/json'];
+export type ClipsFavoriteRequest = operations['clips___favorite']['requestBody']['content']['application/json'];
export type ClipsListResponse = operations['clips___list']['responses']['200']['content']['application/json'];
+export type ClipsMyFavoritesResponse = operations['clips___my-favorites']['responses']['200']['content']['application/json'];
export type ClipsNotesRequest = operations['clips___notes']['requestBody']['content']['application/json'];
export type ClipsNotesResponse = operations['clips___notes']['responses']['200']['content']['application/json'];
+export type ClipsRemoveNoteRequest = operations['clips___remove-note']['requestBody']['content']['application/json'];
export type ClipsShowRequest = operations['clips___show']['requestBody']['content']['application/json'];
export type ClipsShowResponse = operations['clips___show']['responses']['200']['content']['application/json'];
+export type ClipsUnfavoriteRequest = operations['clips___unfavorite']['requestBody']['content']['application/json'];
export type ClipsUpdateRequest = operations['clips___update']['requestBody']['content']['application/json'];
export type ClipsUpdateResponse = operations['clips___update']['responses']['200']['content']['application/json'];
-export type ClipsFavoriteRequest = operations['clips___favorite']['requestBody']['content']['application/json'];
-export type ClipsUnfavoriteRequest = operations['clips___unfavorite']['requestBody']['content']['application/json'];
-export type ClipsMyFavoritesResponse = operations['clips___my-favorites']['responses']['200']['content']['application/json'];
export type DriveResponse = operations['drive']['responses']['200']['content']['application/json'];
export type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json'];
export type DriveFilesResponse = operations['drive___files']['responses']['200']['content']['application/json'];
@@ -234,10 +241,10 @@ export type DriveFilesCheckExistenceResponse = operations['drive___files___check
export type DriveFilesCreateRequest = operations['drive___files___create']['requestBody']['content']['multipart/form-data'];
export type DriveFilesCreateResponse = operations['drive___files___create']['responses']['200']['content']['application/json'];
export type DriveFilesDeleteRequest = operations['drive___files___delete']['requestBody']['content']['application/json'];
-export type DriveFilesFindByHashRequest = operations['drive___files___find-by-hash']['requestBody']['content']['application/json'];
-export type DriveFilesFindByHashResponse = operations['drive___files___find-by-hash']['responses']['200']['content']['application/json'];
export type DriveFilesFindRequest = operations['drive___files___find']['requestBody']['content']['application/json'];
export type DriveFilesFindResponse = operations['drive___files___find']['responses']['200']['content']['application/json'];
+export type DriveFilesFindByHashRequest = operations['drive___files___find-by-hash']['requestBody']['content']['application/json'];
+export type DriveFilesFindByHashResponse = operations['drive___files___find-by-hash']['responses']['200']['content']['application/json'];
export type DriveFilesShowRequest = operations['drive___files___show']['requestBody']['content']['application/json'];
export type DriveFilesShowResponse = operations['drive___files___show']['responses']['200']['content']['application/json'];
export type DriveFilesUpdateRequest = operations['drive___files___update']['requestBody']['content']['application/json'];
@@ -258,6 +265,9 @@ export type DriveStreamRequest = operations['drive___stream']['requestBody']['co
export type DriveStreamResponse = operations['drive___stream']['responses']['200']['content']['application/json'];
export type EmailAddressAvailableRequest = operations['email-address___available']['requestBody']['content']['application/json'];
export type EmailAddressAvailableResponse = operations['email-address___available']['responses']['200']['content']['application/json'];
+export type EmojiRequest = operations['emoji']['requestBody']['content']['application/json'];
+export type EmojiResponse = operations['emoji']['responses']['200']['content']['application/json'];
+export type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json'];
export type EndpointRequest = operations['endpoint']['requestBody']['content']['application/json'];
export type EndpointResponse = operations['endpoint']['responses']['200']['content']['application/json'];
export type EndpointsResponse = operations['endpoints']['responses']['200']['content']['application/json'];
@@ -269,18 +279,33 @@ export type FederationInstancesRequest = operations['federation___instances']['r
export type FederationInstancesResponse = operations['federation___instances']['responses']['200']['content']['application/json'];
export type FederationShowInstanceRequest = operations['federation___show-instance']['requestBody']['content']['application/json'];
export type FederationShowInstanceResponse = operations['federation___show-instance']['responses']['200']['content']['application/json'];
+export type FederationStatsRequest = operations['federation___stats']['requestBody']['content']['application/json'];
+export type FederationStatsResponse = operations['federation___stats']['responses']['200']['content']['application/json'];
export type FederationUpdateRemoteUserRequest = operations['federation___update-remote-user']['requestBody']['content']['application/json'];
export type FederationUsersRequest = operations['federation___users']['requestBody']['content']['application/json'];
export type FederationUsersResponse = operations['federation___users']['responses']['200']['content']['application/json'];
-export type FederationStatsRequest = operations['federation___stats']['requestBody']['content']['application/json'];
-export type FederationStatsResponse = operations['federation___stats']['responses']['200']['content']['application/json'];
+export type FetchExternalResourcesRequest = operations['fetch-external-resources']['requestBody']['content']['application/json'];
+export type FetchExternalResourcesResponse = operations['fetch-external-resources']['responses']['200']['content']['application/json'];
+export type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json'];
+export type FetchRssResponse = operations['fetch-rss']['responses']['200']['content']['application/json'];
+export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
+export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
+export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
+export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
+export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
+export type FlashMyRequest = operations['flash___my']['requestBody']['content']['application/json'];
+export type FlashMyResponse = operations['flash___my']['responses']['200']['content']['application/json'];
+export type FlashMyLikesRequest = operations['flash___my-likes']['requestBody']['content']['application/json'];
+export type FlashMyLikesResponse = operations['flash___my-likes']['responses']['200']['content']['application/json'];
+export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
+export type FlashShowResponse = operations['flash___show']['responses']['200']['content']['application/json'];
+export type FlashUnlikeRequest = operations['flash___unlike']['requestBody']['content']['application/json'];
+export type FlashUpdateRequest = operations['flash___update']['requestBody']['content']['application/json'];
export type FollowingCreateRequest = operations['following___create']['requestBody']['content']['application/json'];
export type FollowingCreateResponse = operations['following___create']['responses']['200']['content']['application/json'];
export type FollowingDeleteRequest = operations['following___delete']['requestBody']['content']['application/json'];
export type FollowingDeleteResponse = operations['following___delete']['responses']['200']['content']['application/json'];
-export type FollowingUpdateRequest = operations['following___update']['requestBody']['content']['application/json'];
-export type FollowingUpdateResponse = operations['following___update']['responses']['200']['content']['application/json'];
-export type FollowingUpdateAllRequest = operations['following___update-all']['requestBody']['content']['application/json'];
export type FollowingInvalidateRequest = operations['following___invalidate']['requestBody']['content']['application/json'];
export type FollowingInvalidateResponse = operations['following___invalidate']['responses']['200']['content']['application/json'];
export type FollowingRequestsAcceptRequest = operations['following___requests___accept']['requestBody']['content']['application/json'];
@@ -288,9 +313,12 @@ export type FollowingRequestsCancelRequest = operations['following___requests___
export type FollowingRequestsCancelResponse = operations['following___requests___cancel']['responses']['200']['content']['application/json'];
export type FollowingRequestsListRequest = operations['following___requests___list']['requestBody']['content']['application/json'];
export type FollowingRequestsListResponse = operations['following___requests___list']['responses']['200']['content']['application/json'];
+export type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json'];
export type FollowingRequestsSentRequest = operations['following___requests___sent']['requestBody']['content']['application/json'];
export type FollowingRequestsSentResponse = operations['following___requests___sent']['responses']['200']['content']['application/json'];
-export type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json'];
+export type FollowingUpdateRequest = operations['following___update']['requestBody']['content']['application/json'];
+export type FollowingUpdateResponse = operations['following___update']['responses']['200']['content']['application/json'];
+export type FollowingUpdateAllRequest = operations['following___update-all']['requestBody']['content']['application/json'];
export type GalleryFeaturedRequest = operations['gallery___featured']['requestBody']['content']['application/json'];
export type GalleryFeaturedResponse = operations['gallery___featured']['responses']['200']['content']['application/json'];
export type GalleryPopularResponse = operations['gallery___popular']['responses']['200']['content']['application/json'];
@@ -305,8 +333,8 @@ export type GalleryPostsShowResponse = operations['gallery___posts___show']['res
export type GalleryPostsUnlikeRequest = operations['gallery___posts___unlike']['requestBody']['content']['application/json'];
export type GalleryPostsUpdateRequest = operations['gallery___posts___update']['requestBody']['content']['application/json'];
export type GalleryPostsUpdateResponse = operations['gallery___posts___update']['responses']['200']['content']['application/json'];
-export type GetOnlineUsersCountResponse = operations['get-online-users-count']['responses']['200']['content']['application/json'];
export type GetAvatarDecorationsResponse = operations['get-avatar-decorations']['responses']['200']['content']['application/json'];
+export type GetOnlineUsersCountResponse = operations['get-online-users-count']['responses']['200']['content']['application/json'];
export type HashtagsListRequest = operations['hashtags___list']['requestBody']['content']['application/json'];
export type HashtagsListResponse = operations['hashtags___list']['responses']['200']['content']['application/json'];
export type HashtagsSearchRequest = operations['hashtags___search']['requestBody']['content']['application/json'];
@@ -322,19 +350,19 @@ export type I2faDoneResponse = operations['i___2fa___done']['responses']['200'][
export type I2faKeyDoneRequest = operations['i___2fa___key-done']['requestBody']['content']['application/json'];
export type I2faKeyDoneResponse = operations['i___2fa___key-done']['responses']['200']['content']['application/json'];
export type I2faPasswordLessRequest = operations['i___2fa___password-less']['requestBody']['content']['application/json'];
-export type I2faRegisterKeyRequest = operations['i___2fa___register-key']['requestBody']['content']['application/json'];
-export type I2faRegisterKeyResponse = operations['i___2fa___register-key']['responses']['200']['content']['application/json'];
export type I2faRegisterRequest = operations['i___2fa___register']['requestBody']['content']['application/json'];
export type I2faRegisterResponse = operations['i___2fa___register']['responses']['200']['content']['application/json'];
-export type I2faUpdateKeyRequest = operations['i___2fa___update-key']['requestBody']['content']['application/json'];
+export type I2faRegisterKeyRequest = operations['i___2fa___register-key']['requestBody']['content']['application/json'];
+export type I2faRegisterKeyResponse = operations['i___2fa___register-key']['responses']['200']['content']['application/json'];
export type I2faRemoveKeyRequest = operations['i___2fa___remove-key']['requestBody']['content']['application/json'];
export type I2faUnregisterRequest = operations['i___2fa___unregister']['requestBody']['content']['application/json'];
+export type I2faUpdateKeyRequest = operations['i___2fa___update-key']['requestBody']['content']['application/json'];
export type IAppsRequest = operations['i___apps']['requestBody']['content']['application/json'];
export type IAppsResponse = operations['i___apps']['responses']['200']['content']['application/json'];
export type IAuthorizedAppsRequest = operations['i___authorized-apps']['requestBody']['content']['application/json'];
export type IAuthorizedAppsResponse = operations['i___authorized-apps']['responses']['200']['content']['application/json'];
-export type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json'];
export type IChangePasswordRequest = operations['i___change-password']['requestBody']['content']['application/json'];
+export type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json'];
export type IDeleteAccountRequest = operations['i___delete-account']['requestBody']['content']['application/json'];
export type IExportFollowingRequest = operations['i___export-following']['requestBody']['content']['application/json'];
export type IFavoritesRequest = operations['i___favorites']['requestBody']['content']['application/json'];
@@ -343,12 +371,14 @@ export type IGalleryLikesRequest = operations['i___gallery___likes']['requestBod
export type IGalleryLikesResponse = operations['i___gallery___likes']['responses']['200']['content']['application/json'];
export type IGalleryPostsRequest = operations['i___gallery___posts']['requestBody']['content']['application/json'];
export type IGalleryPostsResponse = operations['i___gallery___posts']['responses']['200']['content']['application/json'];
+export type IImportAntennasRequest = operations['i___import-antennas']['requestBody']['content']['application/json'];
export type IImportBlockingRequest = operations['i___import-blocking']['requestBody']['content']['application/json'];
export type IImportFollowingRequest = operations['i___import-following']['requestBody']['content']['application/json'];
-export type IImportNotesRequest = operations['i___import-notes']['requestBody']['content']['application/json'];
export type IImportMutingRequest = operations['i___import-muting']['requestBody']['content']['application/json'];
+export type IImportNotesRequest = operations['i___import-notes']['requestBody']['content']['application/json'];
export type IImportUserListsRequest = operations['i___import-user-lists']['requestBody']['content']['application/json'];
-export type IImportAntennasRequest = operations['i___import-antennas']['requestBody']['content']['application/json'];
+export type IMoveRequest = operations['i___move']['requestBody']['content']['application/json'];
+export type IMoveResponse = operations['i___move']['responses']['200']['content']['application/json'];
export type INotificationsRequest = operations['i___notifications']['requestBody']['content']['application/json'];
export type INotificationsResponse = operations['i___notifications']['responses']['200']['content']['application/json'];
export type INotificationsGroupedRequest = operations['i___notifications-grouped']['requestBody']['content']['application/json'];
@@ -361,17 +391,17 @@ export type IPinRequest = operations['i___pin']['requestBody']['content']['appli
export type IPinResponse = operations['i___pin']['responses']['200']['content']['application/json'];
export type IReadAnnouncementRequest = operations['i___read-announcement']['requestBody']['content']['application/json'];
export type IRegenerateTokenRequest = operations['i___regenerate-token']['requestBody']['content']['application/json'];
+export type IRegistryGetRequest = operations['i___registry___get']['requestBody']['content']['application/json'];
+export type IRegistryGetResponse = operations['i___registry___get']['responses']['200']['content']['application/json'];
export type IRegistryGetAllRequest = operations['i___registry___get-all']['requestBody']['content']['application/json'];
export type IRegistryGetAllResponse = operations['i___registry___get-all']['responses']['200']['content']['application/json'];
-export type IRegistryGetUnsecureRequest = operations['i___registry___get-unsecure']['requestBody']['content']['application/json'];
export type IRegistryGetDetailRequest = operations['i___registry___get-detail']['requestBody']['content']['application/json'];
export type IRegistryGetDetailResponse = operations['i___registry___get-detail']['responses']['200']['content']['application/json'];
-export type IRegistryGetRequest = operations['i___registry___get']['requestBody']['content']['application/json'];
-export type IRegistryGetResponse = operations['i___registry___get']['responses']['200']['content']['application/json'];
-export type IRegistryKeysWithTypeRequest = operations['i___registry___keys-with-type']['requestBody']['content']['application/json'];
-export type IRegistryKeysWithTypeResponse = operations['i___registry___keys-with-type']['responses']['200']['content']['application/json'];
+export type IRegistryGetUnsecureRequest = operations['i___registry___get-unsecure']['requestBody']['content']['application/json'];
export type IRegistryKeysRequest = operations['i___registry___keys']['requestBody']['content']['application/json'];
export type IRegistryKeysResponse = operations['i___registry___keys']['responses']['200']['content']['application/json'];
+export type IRegistryKeysWithTypeRequest = operations['i___registry___keys-with-type']['requestBody']['content']['application/json'];
+export type IRegistryKeysWithTypeResponse = operations['i___registry___keys-with-type']['responses']['200']['content']['application/json'];
export type IRegistryRemoveRequest = operations['i___registry___remove']['requestBody']['content']['application/json'];
export type IRegistryScopesWithDomainResponse = operations['i___registry___scopes-with-domain']['responses']['200']['content']['application/json'];
export type IRegistrySetRequest = operations['i___registry___set']['requestBody']['content']['application/json'];
@@ -380,44 +410,37 @@ export type ISigninHistoryRequest = operations['i___signin-history']['requestBod
export type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json'];
export type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];
export type IUnpinResponse = operations['i___unpin']['responses']['200']['content']['application/json'];
-export type IUpdateEmailRequest = operations['i___update-email']['requestBody']['content']['application/json'];
-export type IUpdateEmailResponse = operations['i___update-email']['responses']['200']['content']['application/json'];
export type IUpdateRequest = operations['i___update']['requestBody']['content']['application/json'];
export type IUpdateResponse = operations['i___update']['responses']['200']['content']['application/json'];
-export type IMoveRequest = operations['i___move']['requestBody']['content']['application/json'];
-export type IMoveResponse = operations['i___move']['responses']['200']['content']['application/json'];
+export type IUpdateEmailRequest = operations['i___update-email']['requestBody']['content']['application/json'];
+export type IUpdateEmailResponse = operations['i___update-email']['responses']['200']['content']['application/json'];
export type IWebhooksCreateRequest = operations['i___webhooks___create']['requestBody']['content']['application/json'];
export type IWebhooksCreateResponse = operations['i___webhooks___create']['responses']['200']['content']['application/json'];
+export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json'];
export type IWebhooksListResponse = operations['i___webhooks___list']['responses']['200']['content']['application/json'];
export type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['content']['application/json'];
export type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
-export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
-export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json'];
export type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
+export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
export type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json'];
export type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json'];
+export type InviteLimitResponse = operations['invite___limit']['responses']['200']['content']['application/json'];
export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json'];
export type InviteListResponse = operations['invite___list']['responses']['200']['content']['application/json'];
-export type InviteLimitResponse = operations['invite___limit']['responses']['200']['content']['application/json'];
export type MetaRequest = operations['meta']['requestBody']['content']['application/json'];
export type MetaResponse = operations['meta']['responses']['200']['content']['application/json'];
-export type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json'];
-export type EmojiRequest = operations['emoji']['requestBody']['content']['application/json'];
-export type EmojiResponse = operations['emoji']['responses']['200']['content']['application/json'];
export type MiauthGenTokenRequest = operations['miauth___gen-token']['requestBody']['content']['application/json'];
export type MiauthGenTokenResponse = operations['miauth___gen-token']['responses']['200']['content']['application/json'];
export type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
export type MuteDeleteRequest = operations['mute___delete']['requestBody']['content']['application/json'];
export type MuteListRequest = operations['mute___list']['requestBody']['content']['application/json'];
export type MuteListResponse = operations['mute___list']['responses']['200']['content']['application/json'];
-export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];
-export type RenoteMuteDeleteRequest = operations['renote-mute___delete']['requestBody']['content']['application/json'];
-export type RenoteMuteListRequest = operations['renote-mute___list']['requestBody']['content']['application/json'];
-export type RenoteMuteListResponse = operations['renote-mute___list']['responses']['200']['content']['application/json'];
export type MyAppsRequest = operations['my___apps']['requestBody']['content']['application/json'];
export type MyAppsResponse = operations['my___apps']['responses']['200']['content']['application/json'];
export type NotesRequest = operations['notes']['requestBody']['content']['application/json'];
export type NotesResponse = operations['notes']['responses']['200']['content']['application/json'];
+export type NotesBubbleTimelineRequest = operations['notes___bubble-timeline']['requestBody']['content']['application/json'];
+export type NotesBubbleTimelineResponse = operations['notes___bubble-timeline']['responses']['200']['content']['application/json'];
export type NotesChildrenRequest = operations['notes___children']['requestBody']['content']['application/json'];
export type NotesChildrenResponse = operations['notes___children']['responses']['200']['content']['application/json'];
export type NotesClipsRequest = operations['notes___clips']['requestBody']['content']['application/json'];
@@ -427,6 +450,8 @@ export type NotesConversationResponse = operations['notes___conversation']['resp
export type NotesCreateRequest = operations['notes___create']['requestBody']['content']['application/json'];
export type NotesCreateResponse = operations['notes___create']['responses']['200']['content']['application/json'];
export type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json'];
+export type NotesEditRequest = operations['notes___edit']['requestBody']['content']['application/json'];
+export type NotesEditResponse = operations['notes___edit']['responses']['200']['content']['application/json'];
export type NotesFavoritesCreateRequest = operations['notes___favorites___create']['requestBody']['content']['application/json'];
export type NotesFavoritesDeleteRequest = operations['notes___favorites___delete']['requestBody']['content']['application/json'];
export type NotesFeaturedRequest = operations['notes___featured']['requestBody']['content']['application/json'];
@@ -435,23 +460,21 @@ export type NotesFollowingRequest = operations['notes___following']['requestBody
export type NotesFollowingResponse = operations['notes___following']['responses']['200']['content']['application/json'];
export type NotesGlobalTimelineRequest = operations['notes___global-timeline']['requestBody']['content']['application/json'];
export type NotesGlobalTimelineResponse = operations['notes___global-timeline']['responses']['200']['content']['application/json'];
-export type NotesBubbleTimelineRequest = operations['notes___bubble-timeline']['requestBody']['content']['application/json'];
-export type NotesBubbleTimelineResponse = operations['notes___bubble-timeline']['responses']['200']['content']['application/json'];
export type NotesHybridTimelineRequest = operations['notes___hybrid-timeline']['requestBody']['content']['application/json'];
export type NotesHybridTimelineResponse = operations['notes___hybrid-timeline']['responses']['200']['content']['application/json'];
+export type NotesLikeRequest = operations['notes___like']['requestBody']['content']['application/json'];
export type NotesLocalTimelineRequest = operations['notes___local-timeline']['requestBody']['content']['application/json'];
export type NotesLocalTimelineResponse = operations['notes___local-timeline']['responses']['200']['content']['application/json'];
export type NotesMentionsRequest = operations['notes___mentions']['requestBody']['content']['application/json'];
export type NotesMentionsResponse = operations['notes___mentions']['responses']['200']['content']['application/json'];
export type NotesPollsRecommendationRequest = operations['notes___polls___recommendation']['requestBody']['content']['application/json'];
export type NotesPollsRecommendationResponse = operations['notes___polls___recommendation']['responses']['200']['content']['application/json'];
-export type NotesPollsVoteRequest = operations['notes___polls___vote']['requestBody']['content']['application/json'];
export type NotesPollsRefreshRequest = operations['notes___polls___refresh']['requestBody']['content']['application/json'];
+export type NotesPollsVoteRequest = operations['notes___polls___vote']['requestBody']['content']['application/json'];
export type NotesReactionsRequest = operations['notes___reactions']['requestBody']['content']['application/json'];
export type NotesReactionsResponse = operations['notes___reactions']['responses']['200']['content']['application/json'];
export type NotesReactionsCreateRequest = operations['notes___reactions___create']['requestBody']['content']['application/json'];
export type NotesReactionsDeleteRequest = operations['notes___reactions___delete']['requestBody']['content']['application/json'];
-export type NotesLikeRequest = operations['notes___like']['requestBody']['content']['application/json'];
export type NotesRenotesRequest = operations['notes___renotes']['requestBody']['content']['application/json'];
export type NotesRenotesResponse = operations['notes___renotes']['responses']['200']['content']['application/json'];
export type NotesRepliesRequest = operations['notes___replies']['requestBody']['content']['application/json'];
@@ -460,10 +483,10 @@ export type NotesScheduleCreateRequest = operations['notes___schedule___create']
export type NotesScheduleDeleteRequest = operations['notes___schedule___delete']['requestBody']['content']['application/json'];
export type NotesScheduleListRequest = operations['notes___schedule___list']['requestBody']['content']['application/json'];
export type NotesScheduleListResponse = operations['notes___schedule___list']['responses']['200']['content']['application/json'];
-export type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json'];
-export type NotesSearchByTagResponse = operations['notes___search-by-tag']['responses']['200']['content']['application/json'];
export type NotesSearchRequest = operations['notes___search']['requestBody']['content']['application/json'];
export type NotesSearchResponse = operations['notes___search']['responses']['200']['content']['application/json'];
+export type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json'];
+export type NotesSearchByTagResponse = operations['notes___search-by-tag']['responses']['200']['content']['application/json'];
export type NotesShowRequest = operations['notes___show']['requestBody']['content']['application/json'];
export type NotesShowResponse = operations['notes___show']['responses']['200']['content']['application/json'];
export type NotesStateRequest = operations['notes___state']['requestBody']['content']['application/json'];
@@ -477,8 +500,6 @@ export type NotesTranslateResponse = operations['notes___translate']['responses'
export type NotesUnrenoteRequest = operations['notes___unrenote']['requestBody']['content']['application/json'];
export type NotesUserListTimelineRequest = operations['notes___user-list-timeline']['requestBody']['content']['application/json'];
export type NotesUserListTimelineResponse = operations['notes___user-list-timeline']['responses']['200']['content']['application/json'];
-export type NotesEditRequest = operations['notes___edit']['requestBody']['content']['application/json'];
-export type NotesEditResponse = operations['notes___edit']['responses']['200']['content']['application/json'];
export type NotesVersionsRequest = operations['notes___versions']['requestBody']['content']['application/json'];
export type NotesVersionsResponse = operations['notes___versions']['responses']['200']['content']['application/json'];
export type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
@@ -492,49 +513,58 @@ export type PagesShowRequest = operations['pages___show']['requestBody']['conten
export type PagesShowResponse = operations['pages___show']['responses']['200']['content']['application/json'];
export type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content']['application/json'];
export type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json'];
-export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
-export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
-export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
-export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
-export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
-export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
-export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
-export type FlashShowResponse = operations['flash___show']['responses']['200']['content']['application/json'];
-export type FlashUnlikeRequest = operations['flash___unlike']['requestBody']['content']['application/json'];
-export type FlashUpdateRequest = operations['flash___update']['requestBody']['content']['application/json'];
-export type FlashMyRequest = operations['flash___my']['requestBody']['content']['application/json'];
-export type FlashMyResponse = operations['flash___my']['responses']['200']['content']['application/json'];
-export type FlashMyLikesRequest = operations['flash___my-likes']['requestBody']['content']['application/json'];
-export type FlashMyLikesResponse = operations['flash___my-likes']['responses']['200']['content']['application/json'];
export type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
export type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json'];
export type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
+export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];
+export type RenoteMuteDeleteRequest = operations['renote-mute___delete']['requestBody']['content']['application/json'];
+export type RenoteMuteListRequest = operations['renote-mute___list']['requestBody']['content']['application/json'];
+export type RenoteMuteListResponse = operations['renote-mute___list']['responses']['200']['content']['application/json'];
+export type RequestResetPasswordRequest = operations['request-reset-password']['requestBody']['content']['application/json'];
+export type ResetPasswordRequest = operations['reset-password']['requestBody']['content']['application/json'];
+export type RetentionResponse = operations['retention']['responses']['200']['content']['application/json'];
+export type ReversiCancelMatchRequest = operations['reversi___cancel-match']['requestBody']['content']['application/json'];
+export type ReversiGamesRequest = operations['reversi___games']['requestBody']['content']['application/json'];
+export type ReversiGamesResponse = operations['reversi___games']['responses']['200']['content']['application/json'];
+export type ReversiInvitationsResponse = operations['reversi___invitations']['responses']['200']['content']['application/json'];
+export type ReversiMatchRequest = operations['reversi___match']['requestBody']['content']['application/json'];
+export type ReversiMatchResponse = operations['reversi___match']['responses']['200']['content']['application/json'];
+export type ReversiShowGameRequest = operations['reversi___show-game']['requestBody']['content']['application/json'];
+export type ReversiShowGameResponse = operations['reversi___show-game']['responses']['200']['content']['application/json'];
+export type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json'];
+export type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json'];
+export type ReversiVerifyResponse = operations['reversi___verify']['responses']['200']['content']['application/json'];
export type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json'];
+export type RolesNotesRequest = operations['roles___notes']['requestBody']['content']['application/json'];
+export type RolesNotesResponse = operations['roles___notes']['responses']['200']['content']['application/json'];
export type RolesShowRequest = operations['roles___show']['requestBody']['content']['application/json'];
export type RolesShowResponse = operations['roles___show']['responses']['200']['content']['application/json'];
export type RolesUsersRequest = operations['roles___users']['requestBody']['content']['application/json'];
export type RolesUsersResponse = operations['roles___users']['responses']['200']['content']['application/json'];
-export type RolesNotesRequest = operations['roles___notes']['requestBody']['content']['application/json'];
-export type RolesNotesResponse = operations['roles___notes']['responses']['200']['content']['application/json'];
-export type RequestResetPasswordRequest = operations['request-reset-password']['requestBody']['content']['application/json'];
-export type ResetPasswordRequest = operations['reset-password']['requestBody']['content']['application/json'];
export type ServerInfoResponse = operations['server-info']['responses']['200']['content']['application/json'];
+export type SponsorsRequest = operations['sponsors']['requestBody']['content']['application/json'];
export type StatsResponse = operations['stats']['responses']['200']['content']['application/json'];
+export type SwRegisterRequest = operations['sw___register']['requestBody']['content']['application/json'];
+export type SwRegisterResponse = operations['sw___register']['responses']['200']['content']['application/json'];
export type SwShowRegistrationRequest = operations['sw___show-registration']['requestBody']['content']['application/json'];
export type SwShowRegistrationResponse = operations['sw___show-registration']['responses']['200']['content']['application/json'];
+export type SwUnregisterRequest = operations['sw___unregister']['requestBody']['content']['application/json'];
export type SwUpdateRegistrationRequest = operations['sw___update-registration']['requestBody']['content']['application/json'];
export type SwUpdateRegistrationResponse = operations['sw___update-registration']['responses']['200']['content']['application/json'];
-export type SwRegisterRequest = operations['sw___register']['requestBody']['content']['application/json'];
-export type SwRegisterResponse = operations['sw___register']['responses']['200']['content']['application/json'];
-export type SwUnregisterRequest = operations['sw___unregister']['requestBody']['content']['application/json'];
export type TestRequest = operations['test']['requestBody']['content']['application/json'];
export type TestResponse = operations['test']['responses']['200']['content']['application/json'];
export type UsernameAvailableRequest = operations['username___available']['requestBody']['content']['application/json'];
export type UsernameAvailableResponse = operations['username___available']['responses']['200']['content']['application/json'];
export type UsersRequest = operations['users']['requestBody']['content']['application/json'];
export type UsersResponse = operations['users']['responses']['200']['content']['application/json'];
+export type UsersAchievementsRequest = operations['users___achievements']['requestBody']['content']['application/json'];
+export type UsersAchievementsResponse = operations['users___achievements']['responses']['200']['content']['application/json'];
export type UsersClipsRequest = operations['users___clips']['requestBody']['content']['application/json'];
export type UsersClipsResponse = operations['users___clips']['responses']['200']['content']['application/json'];
+export type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json'];
+export type UsersFeaturedNotesResponse = operations['users___featured-notes']['responses']['200']['content']['application/json'];
+export type UsersFlashsRequest = operations['users___flashs']['requestBody']['content']['application/json'];
+export type UsersFlashsResponse = operations['users___flashs']['responses']['200']['content']['application/json'];
export type UsersFollowersRequest = operations['users___followers']['requestBody']['content']['application/json'];
export type UsersFollowersResponse = operations['users___followers']['responses']['200']['content']['application/json'];
export type UsersFollowingRequest = operations['users___following']['requestBody']['content']['application/json'];
@@ -543,32 +573,28 @@ export type UsersGalleryPostsRequest = operations['users___gallery___posts']['re
export type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
export type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
export type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json'];
-export type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json'];
-export type UsersFeaturedNotesResponse = operations['users___featured-notes']['responses']['200']['content']['application/json'];
export type UsersListsCreateRequest = operations['users___lists___create']['requestBody']['content']['application/json'];
export type UsersListsCreateResponse = operations['users___lists___create']['responses']['200']['content']['application/json'];
+export type UsersListsCreateFromPublicRequest = operations['users___lists___create-from-public']['requestBody']['content']['application/json'];
+export type UsersListsCreateFromPublicResponse = operations['users___lists___create-from-public']['responses']['200']['content']['application/json'];
export type UsersListsDeleteRequest = operations['users___lists___delete']['requestBody']['content']['application/json'];
+export type UsersListsFavoriteRequest = operations['users___lists___favorite']['requestBody']['content']['application/json'];
+export type UsersListsGetMembershipsRequest = operations['users___lists___get-memberships']['requestBody']['content']['application/json'];
+export type UsersListsGetMembershipsResponse = operations['users___lists___get-memberships']['responses']['200']['content']['application/json'];
export type UsersListsListRequest = operations['users___lists___list']['requestBody']['content']['application/json'];
export type UsersListsListResponse = operations['users___lists___list']['responses']['200']['content']['application/json'];
export type UsersListsPullRequest = operations['users___lists___pull']['requestBody']['content']['application/json'];
export type UsersListsPushRequest = operations['users___lists___push']['requestBody']['content']['application/json'];
export type UsersListsShowRequest = operations['users___lists___show']['requestBody']['content']['application/json'];
export type UsersListsShowResponse = operations['users___lists___show']['responses']['200']['content']['application/json'];
-export type UsersListsFavoriteRequest = operations['users___lists___favorite']['requestBody']['content']['application/json'];
export type UsersListsUnfavoriteRequest = operations['users___lists___unfavorite']['requestBody']['content']['application/json'];
export type UsersListsUpdateRequest = operations['users___lists___update']['requestBody']['content']['application/json'];
export type UsersListsUpdateResponse = operations['users___lists___update']['responses']['200']['content']['application/json'];
-export type UsersListsCreateFromPublicRequest = operations['users___lists___create-from-public']['requestBody']['content']['application/json'];
-export type UsersListsCreateFromPublicResponse = operations['users___lists___create-from-public']['responses']['200']['content']['application/json'];
export type UsersListsUpdateMembershipRequest = operations['users___lists___update-membership']['requestBody']['content']['application/json'];
-export type UsersListsGetMembershipsRequest = operations['users___lists___get-memberships']['requestBody']['content']['application/json'];
-export type UsersListsGetMembershipsResponse = operations['users___lists___get-memberships']['responses']['200']['content']['application/json'];
export type UsersNotesRequest = operations['users___notes']['requestBody']['content']['application/json'];
export type UsersNotesResponse = operations['users___notes']['responses']['200']['content']['application/json'];
export type UsersPagesRequest = operations['users___pages']['requestBody']['content']['application/json'];
export type UsersPagesResponse = operations['users___pages']['responses']['200']['content']['application/json'];
-export type UsersFlashsRequest = operations['users___flashs']['requestBody']['content']['application/json'];
-export type UsersFlashsResponse = operations['users___flashs']['responses']['200']['content']['application/json'];
export type UsersReactionsRequest = operations['users___reactions']['requestBody']['content']['application/json'];
export type UsersReactionsResponse = operations['users___reactions']['responses']['200']['content']['application/json'];
export type UsersRecommendationRequest = operations['users___recommendation']['requestBody']['content']['application/json'];
@@ -576,32 +602,12 @@ export type UsersRecommendationResponse = operations['users___recommendation']['
export type UsersRelationRequest = operations['users___relation']['requestBody']['content']['application/json'];
export type UsersRelationResponse = operations['users___relation']['responses']['200']['content']['application/json'];
export type UsersReportAbuseRequest = operations['users___report-abuse']['requestBody']['content']['application/json'];
-export type UsersSearchByUsernameAndHostRequest = operations['users___search-by-username-and-host']['requestBody']['content']['application/json'];
-export type UsersSearchByUsernameAndHostResponse = operations['users___search-by-username-and-host']['responses']['200']['content']['application/json'];
export type UsersSearchRequest = operations['users___search']['requestBody']['content']['application/json'];
export type UsersSearchResponse = operations['users___search']['responses']['200']['content']['application/json'];
+export type UsersSearchByUsernameAndHostRequest = operations['users___search-by-username-and-host']['requestBody']['content']['application/json'];
+export type UsersSearchByUsernameAndHostResponse = operations['users___search-by-username-and-host']['responses']['200']['content']['application/json'];
export type UsersShowRequest = operations['users___show']['requestBody']['content']['application/json'];
export type UsersShowResponse = operations['users___show']['responses']['200']['content']['application/json'];
-export type UsersAchievementsRequest = operations['users___achievements']['requestBody']['content']['application/json'];
-export type UsersAchievementsResponse = operations['users___achievements']['responses']['200']['content']['application/json'];
export type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json'];
-export type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json'];
-export type FetchRssResponse = operations['fetch-rss']['responses']['200']['content']['application/json'];
-export type FetchExternalResourcesRequest = operations['fetch-external-resources']['requestBody']['content']['application/json'];
-export type FetchExternalResourcesResponse = operations['fetch-external-resources']['responses']['200']['content']['application/json'];
-export type RetentionResponse = operations['retention']['responses']['200']['content']['application/json'];
-export type SponsorsRequest = operations['sponsors']['requestBody']['content']['application/json'];
-export type BubbleGameRegisterRequest = operations['bubble-game___register']['requestBody']['content']['application/json'];
-export type BubbleGameRankingRequest = operations['bubble-game___ranking']['requestBody']['content']['application/json'];
-export type BubbleGameRankingResponse = operations['bubble-game___ranking']['responses']['200']['content']['application/json'];
-export type ReversiCancelMatchRequest = operations['reversi___cancel-match']['requestBody']['content']['application/json'];
-export type ReversiGamesRequest = operations['reversi___games']['requestBody']['content']['application/json'];
-export type ReversiGamesResponse = operations['reversi___games']['responses']['200']['content']['application/json'];
-export type ReversiMatchRequest = operations['reversi___match']['requestBody']['content']['application/json'];
-export type ReversiMatchResponse = operations['reversi___match']['responses']['200']['content']['application/json'];
-export type ReversiInvitationsResponse = operations['reversi___invitations']['responses']['200']['content']['application/json'];
-export type ReversiShowGameRequest = operations['reversi___show-game']['requestBody']['content']['application/json'];
-export type ReversiShowGameResponse = operations['reversi___show-game']['responses']['200']['content']['application/json'];
-export type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json'];
-export type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json'];
-export type ReversiVerifyResponse = operations['reversi___verify']['responses']['200']['content']['application/json'];
+export type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestBody']['content']['application/json'];
+export type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['responses']['200']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index 04574849d4..1a30da4437 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -33,6 +33,7 @@ export type FederationInstance = components['schemas']['FederationInstance'];
export type GalleryPost = components['schemas']['GalleryPost'];
export type EmojiSimple = components['schemas']['EmojiSimple'];
export type EmojiDetailed = components['schemas']['EmojiDetailed'];
+export type EmojiDetailedAdmin = components['schemas']['EmojiDetailedAdmin'];
export type Flash = components['schemas']['Flash'];
export type Signin = components['schemas']['Signin'];
export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 888e46e008..d58607bb3b 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -12,23 +12,25 @@ type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> &
type OneOf<T extends any[]> = T extends [infer Only] ? Only : T extends [infer A, infer B, ...infer Rest] ? OneOf<[XOR<A, B>, ...Rest]> : never;
export type paths = {
- '/admin/meta': {
+ '/admin/abuse-report/notification-recipient/create': {
/**
- * admin/meta
+ * admin/abuse-report/notification-recipient/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
*/
- post: operations['admin___meta'];
+ post: operations['admin___abuse-report___notification-recipient___create'];
};
- '/admin/abuse-user-reports': {
+ '/admin/abuse-report/notification-recipient/delete': {
/**
- * admin/abuse-user-reports
+ * admin/abuse-report/notification-recipient/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
*/
- post: operations['admin___abuse-user-reports'];
+ post: operations['admin___abuse-report___notification-recipient___delete'];
};
'/admin/abuse-report/notification-recipient/list': {
/**
@@ -50,16 +52,6 @@ export type paths = {
*/
post: operations['admin___abuse-report___notification-recipient___show'];
};
- '/admin/abuse-report/notification-recipient/create': {
- /**
- * admin/abuse-report/notification-recipient/create
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
- */
- post: operations['admin___abuse-report___notification-recipient___create'];
- };
'/admin/abuse-report/notification-recipient/update': {
/**
* admin/abuse-report/notification-recipient/update
@@ -70,15 +62,14 @@ export type paths = {
*/
post: operations['admin___abuse-report___notification-recipient___update'];
};
- '/admin/abuse-report/notification-recipient/delete': {
+ '/admin/abuse-user-reports': {
/**
- * admin/abuse-report/notification-recipient/delete
+ * admin/abuse-user-reports
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports*
*/
- post: operations['admin___abuse-report___notification-recipient___delete'];
+ post: operations['admin___abuse-user-reports'];
};
'/admin/accounts/create': {
/**
@@ -179,6 +170,15 @@ export type paths = {
*/
post: operations['admin___announcements___update'];
};
+ '/admin/approve-user': {
+ /**
+ * admin/approve-user
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
+ */
+ post: operations['admin___approve-user'];
+ };
'/admin/avatar-decorations/create': {
/**
* admin/avatar-decorations/create
@@ -215,32 +215,59 @@ export type paths = {
*/
post: operations['admin___avatar-decorations___update'];
};
- '/admin/delete-all-files-of-a-user': {
+ '/admin/captcha/current': {
/**
- * admin/delete-all-files-of-a-user
+ * admin/captcha/current
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
*/
- post: operations['admin___delete-all-files-of-a-user'];
+ post: operations['admin___captcha___current'];
};
- '/admin/unset-user-avatar': {
+ '/admin/captcha/save': {
/**
- * admin/unset-user-avatar
+ * admin/captcha/save
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
*/
- post: operations['admin___unset-user-avatar'];
+ post: operations['admin___captcha___save'];
};
- '/admin/unset-user-banner': {
+ '/admin/cw-user': {
/**
- * admin/unset-user-banner
+ * admin/cw-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:cw-user*
*/
- post: operations['admin___unset-user-banner'];
+ post: operations['admin___cw-user'];
+ };
+ '/admin/decline-user': {
+ /**
+ * admin/decline-user
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
+ */
+ post: operations['admin___decline-user'];
+ };
+ '/admin/delete-account': {
+ /**
+ * admin/delete-account
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account*
+ */
+ post: operations['admin___delete-account'];
+ };
+ '/admin/delete-all-files-of-a-user': {
+ /**
+ * admin/delete-all-files-of-a-user
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user*
+ */
+ post: operations['admin___delete-all-files-of-a-user'];
};
'/admin/drive/clean-remote-files': {
/**
@@ -278,23 +305,23 @@ export type paths = {
*/
post: operations['admin___drive___show-file'];
};
- '/admin/emoji/add-aliases-bulk': {
+ '/admin/emoji/add': {
/**
- * admin/emoji/add-aliases-bulk
+ * admin/emoji/add
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- post: operations['admin___emoji___add-aliases-bulk'];
+ post: operations['admin___emoji___add'];
};
- '/admin/emoji/add': {
+ '/admin/emoji/add-aliases-bulk': {
/**
- * admin/emoji/add
+ * admin/emoji/add-aliases-bulk
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- post: operations['admin___emoji___add'];
+ post: operations['admin___emoji___add-aliases-bulk'];
};
'/admin/emoji/copy': {
/**
@@ -305,23 +332,23 @@ export type paths = {
*/
post: operations['admin___emoji___copy'];
};
- '/admin/emoji/delete-bulk': {
+ '/admin/emoji/delete': {
/**
- * admin/emoji/delete-bulk
+ * admin/emoji/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- post: operations['admin___emoji___delete-bulk'];
+ post: operations['admin___emoji___delete'];
};
- '/admin/emoji/delete': {
+ '/admin/emoji/delete-bulk': {
/**
- * admin/emoji/delete
+ * admin/emoji/delete-bulk
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- post: operations['admin___emoji___delete'];
+ post: operations['admin___emoji___delete-bulk'];
};
'/admin/emoji/import-zip': {
/**
@@ -333,23 +360,23 @@ export type paths = {
*/
post: operations['admin___emoji___import-zip'];
};
- '/admin/emoji/list-remote': {
+ '/admin/emoji/list': {
/**
- * admin/emoji/list-remote
+ * admin/emoji/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- post: operations['admin___emoji___list-remote'];
+ post: operations['admin___emoji___list'];
};
- '/admin/emoji/list': {
+ '/admin/emoji/list-remote': {
/**
- * admin/emoji/list
+ * admin/emoji/list-remote
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- post: operations['admin___emoji___list'];
+ post: operations['admin___emoji___list-remote'];
};
'/admin/emoji/remove-aliases-bulk': {
/**
@@ -432,6 +459,24 @@ export type paths = {
*/
post: operations['admin___federation___update-instance'];
};
+ '/admin/forward-abuse-user-report': {
+ /**
+ * admin/forward-abuse-user-report
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ post: operations['admin___forward-abuse-user-report'];
+ };
+ '/admin/gen-vapid-keys': {
+ /**
+ * admin/gen-vapid-keys
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
+ */
+ post: operations['admin___gen-vapid-keys'];
+ };
'/admin/get-index-stats': {
/**
* admin/get-index-stats
@@ -477,6 +522,24 @@ export type paths = {
*/
post: operations['admin___invite___list'];
};
+ '/admin/meta': {
+ /**
+ * admin/meta
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
+ */
+ post: operations['admin___meta'];
+ };
+ '/admin/nsfw-user': {
+ /**
+ * admin/nsfw-user
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:nsfw-user*
+ */
+ post: operations['admin___nsfw-user'];
+ };
'/admin/promo/create': {
/**
* admin/promo/create
@@ -531,6 +594,15 @@ export type paths = {
*/
post: operations['admin___queue___stats'];
};
+ '/admin/reject-quotes': {
+ /**
+ * admin/reject-quotes
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:reject-quotes*
+ */
+ post: operations['admin___reject-quotes'];
+ };
'/admin/relays/add': {
/**
* admin/relays/add
@@ -576,308 +648,281 @@ export type paths = {
*/
post: operations['admin___resolve-abuse-user-report'];
};
- '/admin/forward-abuse-user-report': {
- /**
- * admin/forward-abuse-user-report
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
- */
- post: operations['admin___forward-abuse-user-report'];
- };
- '/admin/update-abuse-user-report': {
- /**
- * admin/update-abuse-user-report
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
- */
- post: operations['admin___update-abuse-user-report'];
- };
- '/admin/send-email': {
- /**
- * admin/send-email
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:admin:send-email*
- */
- post: operations['admin___send-email'];
- };
- '/admin/server-info': {
+ '/admin/roles/assign': {
/**
- * admin/server-info
+ * admin/roles/assign
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:server-info*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- post: operations['admin___server-info'];
+ post: operations['admin___roles___assign'];
};
- '/admin/show-moderation-logs': {
+ '/admin/roles/create': {
/**
- * admin/show-moderation-logs
+ * admin/roles/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- post: operations['admin___show-moderation-logs'];
+ post: operations['admin___roles___create'];
};
- '/admin/show-user': {
+ '/admin/roles/delete': {
/**
- * admin/show-user
+ * admin/roles/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- post: operations['admin___show-user'];
+ post: operations['admin___roles___delete'];
};
- '/admin/show-users': {
+ '/admin/roles/list': {
/**
- * admin/show-users
+ * admin/roles/list
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
*/
- post: operations['admin___show-users'];
+ post: operations['admin___roles___list'];
};
- '/admin/nsfw-user': {
+ '/admin/roles/show': {
/**
- * admin/nsfw-user
+ * admin/roles/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:nsfw-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
*/
- post: operations['admin___nsfw-user'];
+ post: operations['admin___roles___show'];
};
- '/admin/unnsfw-user': {
+ '/admin/roles/unassign': {
/**
- * admin/unnsfw-user
+ * admin/roles/unassign
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unnsfw-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- post: operations['admin___unnsfw-user'];
+ post: operations['admin___roles___unassign'];
};
- '/admin/silence-user': {
+ '/admin/roles/update': {
/**
- * admin/silence-user
+ * admin/roles/update
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:silence-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- post: operations['admin___silence-user'];
+ post: operations['admin___roles___update'];
};
- '/admin/unsilence-user': {
+ '/admin/roles/update-default-policies': {
/**
- * admin/unsilence-user
+ * admin/roles/update-default-policies
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unsilence-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- post: operations['admin___unsilence-user'];
+ post: operations['admin___roles___update-default-policies'];
};
- '/admin/suspend-user': {
+ '/admin/roles/users': {
/**
- * admin/suspend-user
+ * admin/roles/users
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
+ * **Credential required**: *No* / **Permission**: *read:admin:roles*
*/
- post: operations['admin___suspend-user'];
+ post: operations['admin___roles___users'];
};
- '/admin/approve-user': {
+ '/admin/send-email': {
/**
- * admin/approve-user
+ * admin/send-email
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:send-email*
*/
- post: operations['admin___approve-user'];
+ post: operations['admin___send-email'];
};
- '/admin/decline-user': {
+ '/admin/server-info': {
/**
- * admin/decline-user
+ * admin/server-info
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:server-info*
*/
- post: operations['admin___decline-user'];
+ post: operations['admin___server-info'];
};
- '/admin/unsuspend-user': {
+ '/admin/show-moderation-logs': {
/**
- * admin/unsuspend-user
+ * admin/show-moderation-logs
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log*
*/
- post: operations['admin___unsuspend-user'];
+ post: operations['admin___show-moderation-logs'];
};
- '/admin/update-meta': {
+ '/admin/show-user': {
/**
- * admin/update-meta
+ * admin/show-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/
- post: operations['admin___update-meta'];
+ post: operations['admin___show-user'];
};
- '/admin/delete-account': {
+ '/admin/show-users': {
/**
- * admin/delete-account
+ * admin/show-users
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/
- post: operations['admin___delete-account'];
+ post: operations['admin___show-users'];
};
- '/admin/update-user-note': {
+ '/admin/silence-user': {
/**
- * admin/update-user-note
+ * admin/silence-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:user-note*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:silence-user*
*/
- post: operations['admin___update-user-note'];
+ post: operations['admin___silence-user'];
};
- '/admin/roles/create': {
+ '/admin/suspend-user': {
/**
- * admin/roles/create
+ * admin/suspend-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
*/
- post: operations['admin___roles___create'];
+ post: operations['admin___suspend-user'];
};
- '/admin/roles/delete': {
+ '/admin/system-webhook/create': {
/**
- * admin/roles/delete
+ * admin/system-webhook/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- post: operations['admin___roles___delete'];
+ post: operations['admin___system-webhook___create'];
};
- '/admin/roles/list': {
+ '/admin/system-webhook/delete': {
/**
- * admin/roles/list
+ * admin/system-webhook/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- post: operations['admin___roles___list'];
+ post: operations['admin___system-webhook___delete'];
};
- '/admin/roles/show': {
+ '/admin/system-webhook/list': {
/**
- * admin/roles/show
+ * admin/system-webhook/list
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- post: operations['admin___roles___show'];
+ post: operations['admin___system-webhook___list'];
};
- '/admin/roles/update': {
+ '/admin/system-webhook/show': {
/**
- * admin/roles/update
+ * admin/system-webhook/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- post: operations['admin___roles___update'];
+ post: operations['admin___system-webhook___show'];
};
- '/admin/roles/assign': {
+ '/admin/system-webhook/test': {
/**
- * admin/roles/assign
+ * admin/system-webhook/test
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
*/
- post: operations['admin___roles___assign'];
+ post: operations['admin___system-webhook___test'];
};
- '/admin/roles/unassign': {
+ '/admin/system-webhook/update': {
/**
- * admin/roles/unassign
+ * admin/system-webhook/update
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- post: operations['admin___roles___unassign'];
+ post: operations['admin___system-webhook___update'];
};
- '/admin/roles/update-default-policies': {
+ '/admin/unnsfw-user': {
/**
- * admin/roles/update-default-policies
+ * admin/unnsfw-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unnsfw-user*
*/
- post: operations['admin___roles___update-default-policies'];
+ post: operations['admin___unnsfw-user'];
};
- '/admin/roles/users': {
+ '/admin/unset-user-avatar': {
/**
- * admin/roles/users
+ * admin/unset-user-avatar
* @description No description provided.
*
- * **Credential required**: *No* / **Permission**: *read:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar*
*/
- post: operations['admin___roles___users'];
+ post: operations['admin___unset-user-avatar'];
};
- '/admin/system-webhook/create': {
+ '/admin/unset-user-banner': {
/**
- * admin/system-webhook/create
+ * admin/unset-user-banner
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner*
*/
- post: operations['admin___system-webhook___create'];
+ post: operations['admin___unset-user-banner'];
};
- '/admin/system-webhook/delete': {
+ '/admin/unsilence-user': {
/**
- * admin/system-webhook/delete
+ * admin/unsilence-user
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unsilence-user*
*/
- post: operations['admin___system-webhook___delete'];
+ post: operations['admin___unsilence-user'];
};
- '/admin/system-webhook/list': {
+ '/admin/unsuspend-user': {
/**
- * admin/system-webhook/list
+ * admin/unsuspend-user
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user*
*/
- post: operations['admin___system-webhook___list'];
+ post: operations['admin___unsuspend-user'];
};
- '/admin/system-webhook/show': {
+ '/admin/update-abuse-user-report': {
/**
- * admin/system-webhook/show
+ * admin/update-abuse-user-report
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
- post: operations['admin___system-webhook___show'];
+ post: operations['admin___update-abuse-user-report'];
};
- '/admin/system-webhook/update': {
+ '/admin/update-meta': {
/**
- * admin/system-webhook/update
+ * admin/update-meta
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
*/
- post: operations['admin___system-webhook___update'];
+ post: operations['admin___update-meta'];
};
- '/admin/system-webhook/test': {
+ '/admin/update-user-note': {
/**
- * admin/system-webhook/test
+ * admin/update-user-note
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:user-note*
*/
- post: operations['admin___system-webhook___test'];
+ post: operations['admin___update-user-note'];
};
'/announcements': {
/**
@@ -1051,6 +1096,31 @@ export type paths = {
*/
post: operations['blocking___list'];
};
+ '/bubble-game/ranking': {
+ /**
+ * bubble-game/ranking
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ get: operations['bubble-game___ranking'];
+ /**
+ * bubble-game/ranking
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['bubble-game___ranking'];
+ };
+ '/bubble-game/register': {
+ /**
+ * bubble-game/register
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:account*
+ */
+ post: operations['bubble-game___register'];
+ };
'/channels/create': {
/**
* channels/create
@@ -1060,6 +1130,15 @@ export type paths = {
*/
post: operations['channels___create'];
};
+ '/channels/favorite': {
+ /**
+ * channels/favorite
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
+ */
+ post: operations['channels___favorite'];
+ };
'/channels/featured': {
/**
* channels/featured
@@ -1087,59 +1166,50 @@ export type paths = {
*/
post: operations['channels___followed'];
};
- '/channels/owned': {
+ '/channels/my-favorites': {
/**
- * channels/owned
+ * channels/my-favorites
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:channels*
*/
- post: operations['channels___owned'];
+ post: operations['channels___my-favorites'];
};
- '/channels/show': {
+ '/channels/owned': {
/**
- * channels/show
+ * channels/owned
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:channels*
*/
- post: operations['channels___show'];
+ post: operations['channels___owned'];
};
- '/channels/timeline': {
+ '/channels/search': {
/**
- * channels/timeline
+ * channels/search
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['channels___timeline'];
- };
- '/channels/unfollow': {
- /**
- * channels/unfollow
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:channels*
- */
- post: operations['channels___unfollow'];
+ post: operations['channels___search'];
};
- '/channels/update': {
+ '/channels/show': {
/**
- * channels/update
+ * channels/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- post: operations['channels___update'];
+ post: operations['channels___show'];
};
- '/channels/favorite': {
+ '/channels/timeline': {
/**
- * channels/favorite
+ * channels/timeline
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- post: operations['channels___favorite'];
+ post: operations['channels___timeline'];
};
'/channels/unfavorite': {
/**
@@ -1150,23 +1220,23 @@ export type paths = {
*/
post: operations['channels___unfavorite'];
};
- '/channels/my-favorites': {
+ '/channels/unfollow': {
/**
- * channels/my-favorites
+ * channels/unfollow
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:channels*
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- post: operations['channels___my-favorites'];
+ post: operations['channels___unfollow'];
};
- '/channels/search': {
+ '/channels/update': {
/**
- * channels/search
+ * channels/update
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- post: operations['channels___search'];
+ post: operations['channels___update'];
};
'/charts/active-users': {
/**
@@ -1369,15 +1439,6 @@ export type paths = {
*/
post: operations['clips___add-note'];
};
- '/clips/remove-note': {
- /**
- * clips/remove-note
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- post: operations['clips___remove-note'];
- };
'/clips/create': {
/**
* clips/create
@@ -1396,6 +1457,15 @@ export type paths = {
*/
post: operations['clips___delete'];
};
+ '/clips/favorite': {
+ /**
+ * clips/favorite
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:clip-favorite*
+ */
+ post: operations['clips___favorite'];
+ };
'/clips/list': {
/**
* clips/list
@@ -1405,41 +1475,41 @@ export type paths = {
*/
post: operations['clips___list'];
};
- '/clips/notes': {
+ '/clips/my-favorites': {
/**
- * clips/notes
+ * clips/my-favorites
* @description No description provided.
*
- * **Credential required**: *No* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *read:clip-favorite*
*/
- post: operations['clips___notes'];
+ post: operations['clips___my-favorites'];
};
- '/clips/show': {
+ '/clips/notes': {
/**
- * clips/show
+ * clips/notes
* @description No description provided.
*
* **Credential required**: *No* / **Permission**: *read:account*
*/
- post: operations['clips___show'];
+ post: operations['clips___notes'];
};
- '/clips/update': {
+ '/clips/remove-note': {
/**
- * clips/update
+ * clips/remove-note
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- post: operations['clips___update'];
+ post: operations['clips___remove-note'];
};
- '/clips/favorite': {
+ '/clips/show': {
/**
- * clips/favorite
+ * clips/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:clip-favorite*
+ * **Credential required**: *No* / **Permission**: *read:account*
*/
- post: operations['clips___favorite'];
+ post: operations['clips___show'];
};
'/clips/unfavorite': {
/**
@@ -1450,14 +1520,14 @@ export type paths = {
*/
post: operations['clips___unfavorite'];
};
- '/clips/my-favorites': {
+ '/clips/update': {
/**
- * clips/my-favorites
+ * clips/update
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:clip-favorite*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- post: operations['clips___my-favorites'];
+ post: operations['clips___update'];
};
'/drive': {
/**
@@ -1513,23 +1583,23 @@ export type paths = {
*/
post: operations['drive___files___delete'];
};
- '/drive/files/find-by-hash': {
+ '/drive/files/find': {
/**
- * drive/files/find-by-hash
- * @description Search for a drive file by a hash of the contents.
+ * drive/files/find
+ * @description Search for a drive file by the given parameters.
*
* **Credential required**: *Yes* / **Permission**: *read:drive*
*/
- post: operations['drive___files___find-by-hash'];
+ post: operations['drive___files___find'];
};
- '/drive/files/find': {
+ '/drive/files/find-by-hash': {
/**
- * drive/files/find
- * @description Search for a drive file by the given parameters.
+ * drive/files/find-by-hash
+ * @description Search for a drive file by a hash of the contents.
*
* **Credential required**: *Yes* / **Permission**: *read:drive*
*/
- post: operations['drive___files___find'];
+ post: operations['drive___files___find-by-hash'];
};
'/drive/files/show': {
/**
@@ -1630,6 +1700,38 @@ export type paths = {
*/
post: operations['email-address___available'];
};
+ '/emoji': {
+ /**
+ * emoji
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ get: operations['emoji'];
+ /**
+ * emoji
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['emoji'];
+ };
+ '/emojis': {
+ /**
+ * emojis
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ get: operations['emojis'];
+ /**
+ * emojis
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['emojis'];
+ };
'/endpoint': {
/**
* endpoint
@@ -1701,6 +1803,22 @@ export type paths = {
*/
post: operations['federation___show-instance'];
};
+ '/federation/stats': {
+ /**
+ * federation/stats
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ get: operations['federation___stats'];
+ /**
+ * federation/stats
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['federation___stats'];
+ };
'/federation/update-remote-user': {
/**
* federation/update-remote-user
@@ -1719,57 +1837,130 @@ export type paths = {
*/
post: operations['federation___users'];
};
- '/federation/stats': {
+ '/fetch-external-resources': {
/**
- * federation/stats
+ * fetch-external-resources
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
+ */
+ post: operations['fetch-external-resources'];
+ };
+ '/fetch-rss': {
+ /**
+ * fetch-rss
* @description No description provided.
*
* **Credential required**: *No*
*/
- get: operations['federation___stats'];
+ get: operations['fetch-rss'];
/**
- * federation/stats
+ * fetch-rss
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['federation___stats'];
+ post: operations['fetch-rss'];
};
- '/following/create': {
+ '/flash/create': {
/**
- * following/create
+ * flash/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:following*
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
*/
- post: operations['following___create'];
+ post: operations['flash___create'];
};
- '/following/delete': {
+ '/flash/delete': {
/**
- * following/delete
+ * flash/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:following*
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
*/
- post: operations['following___delete'];
+ post: operations['flash___delete'];
};
- '/following/update': {
+ '/flash/featured': {
/**
- * following/update
+ * flash/featured
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['flash___featured'];
+ };
+ '/flash/like': {
+ /**
+ * flash/like
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ */
+ post: operations['flash___like'];
+ };
+ '/flash/my': {
+ /**
+ * flash/my
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:flash*
+ */
+ post: operations['flash___my'];
+ };
+ '/flash/my-likes': {
+ /**
+ * flash/my-likes
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:flash-likes*
+ */
+ post: operations['flash___my-likes'];
+ };
+ '/flash/show': {
+ /**
+ * flash/show
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['flash___show'];
+ };
+ '/flash/unlike': {
+ /**
+ * flash/unlike
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ */
+ post: operations['flash___unlike'];
+ };
+ '/flash/update': {
+ /**
+ * flash/update
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
+ */
+ post: operations['flash___update'];
+ };
+ '/following/create': {
+ /**
+ * following/create
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- post: operations['following___update'];
+ post: operations['following___create'];
};
- '/following/update-all': {
+ '/following/delete': {
/**
- * following/update-all
+ * following/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- post: operations['following___update-all'];
+ post: operations['following___delete'];
};
'/following/invalidate': {
/**
@@ -1807,6 +1998,15 @@ export type paths = {
*/
post: operations['following___requests___list'];
};
+ '/following/requests/reject': {
+ /**
+ * following/requests/reject
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:following*
+ */
+ post: operations['following___requests___reject'];
+ };
'/following/requests/sent': {
/**
* following/requests/sent
@@ -1816,14 +2016,23 @@ export type paths = {
*/
post: operations['following___requests___sent'];
};
- '/following/requests/reject': {
+ '/following/update': {
/**
- * following/requests/reject
+ * following/update
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- post: operations['following___requests___reject'];
+ post: operations['following___update'];
+ };
+ '/following/update-all': {
+ /**
+ * following/update-all
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:following*
+ */
+ post: operations['following___update-all'];
};
'/gallery/featured': {
/**
@@ -1906,30 +2115,30 @@ export type paths = {
*/
post: operations['gallery___posts___update'];
};
- '/get-online-users-count': {
+ '/get-avatar-decorations': {
/**
- * get-online-users-count
+ * get-avatar-decorations
* @description No description provided.
*
* **Credential required**: *No*
*/
- get: operations['get-online-users-count'];
+ post: operations['get-avatar-decorations'];
+ };
+ '/get-online-users-count': {
/**
* get-online-users-count
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['get-online-users-count'];
- };
- '/get-avatar-decorations': {
+ get: operations['get-online-users-count'];
/**
- * get-avatar-decorations
+ * get-online-users-count
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['get-avatar-decorations'];
+ post: operations['get-online-users-count'];
};
'/hashtags/list': {
/**
@@ -2022,16 +2231,6 @@ export type paths = {
*/
post: operations['i___2fa___password-less'];
};
- '/i/2fa/register-key': {
- /**
- * i/2fa/register-key
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
- */
- post: operations['i___2fa___register-key'];
- };
'/i/2fa/register': {
/**
* i/2fa/register
@@ -2042,15 +2241,15 @@ export type paths = {
*/
post: operations['i___2fa___register'];
};
- '/i/2fa/update-key': {
+ '/i/2fa/register-key': {
/**
- * i/2fa/update-key
+ * i/2fa/register-key
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___2fa___update-key'];
+ post: operations['i___2fa___register-key'];
};
'/i/2fa/remove-key': {
/**
@@ -2072,6 +2271,16 @@ export type paths = {
*/
post: operations['i___2fa___unregister'];
};
+ '/i/2fa/update-key': {
+ /**
+ * i/2fa/update-key
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
+ */
+ post: operations['i___2fa___update-key'];
+ };
'/i/apps': {
/**
* i/apps
@@ -2092,15 +2301,6 @@ export type paths = {
*/
post: operations['i___authorized-apps'];
};
- '/i/claim-achievement': {
- /**
- * i/claim-achievement
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- post: operations['i___claim-achievement'];
- };
'/i/change-password': {
/**
* i/change-password
@@ -2111,6 +2311,15 @@ export type paths = {
*/
post: operations['i___change-password'];
};
+ '/i/claim-achievement': {
+ /**
+ * i/claim-achievement
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:account*
+ */
+ post: operations['i___claim-achievement'];
+ };
'/i/delete-account': {
/**
* i/delete-account
@@ -2121,15 +2330,15 @@ export type paths = {
*/
post: operations['i___delete-account'];
};
- '/i/export-data': {
+ '/i/export-antennas': {
/**
- * i/export-data
+ * i/export-antennas
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___export-data'];
+ post: operations['i___export-antennas'];
};
'/i/export-blocking': {
/**
@@ -2141,75 +2350,75 @@ export type paths = {
*/
post: operations['i___export-blocking'];
};
- '/i/export-following': {
+ '/i/export-clips': {
/**
- * i/export-following
+ * i/export-clips
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___export-following'];
+ post: operations['i___export-clips'];
};
- '/i/export-mute': {
+ '/i/export-data': {
/**
- * i/export-mute
+ * i/export-data
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___export-mute'];
+ post: operations['i___export-data'];
};
- '/i/export-notes': {
+ '/i/export-favorites': {
/**
- * i/export-notes
+ * i/export-favorites
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___export-notes'];
+ post: operations['i___export-favorites'];
};
- '/i/export-clips': {
+ '/i/export-following': {
/**
- * i/export-clips
+ * i/export-following
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___export-clips'];
+ post: operations['i___export-following'];
};
- '/i/export-favorites': {
+ '/i/export-mute': {
/**
- * i/export-favorites
+ * i/export-mute
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___export-favorites'];
+ post: operations['i___export-mute'];
};
- '/i/export-user-lists': {
+ '/i/export-notes': {
/**
- * i/export-user-lists
+ * i/export-notes
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___export-user-lists'];
+ post: operations['i___export-notes'];
};
- '/i/export-antennas': {
+ '/i/export-user-lists': {
/**
- * i/export-antennas
+ * i/export-user-lists
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___export-antennas'];
+ post: operations['i___export-user-lists'];
};
'/i/favorites': {
/**
@@ -2238,6 +2447,16 @@ export type paths = {
*/
post: operations['i___gallery___posts'];
};
+ '/i/import-antennas': {
+ /**
+ * i/import-antennas
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
+ */
+ post: operations['i___import-antennas'];
+ };
'/i/import-blocking': {
/**
* i/import-blocking
@@ -2258,25 +2477,25 @@ export type paths = {
*/
post: operations['i___import-following'];
};
- '/i/import-notes': {
+ '/i/import-muting': {
/**
- * i/import-notes
+ * i/import-muting
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___import-notes'];
+ post: operations['i___import-muting'];
};
- '/i/import-muting': {
+ '/i/import-notes': {
/**
- * i/import-muting
+ * i/import-notes
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___import-muting'];
+ post: operations['i___import-notes'];
};
'/i/import-user-lists': {
/**
@@ -2288,15 +2507,15 @@ export type paths = {
*/
post: operations['i___import-user-lists'];
};
- '/i/import-antennas': {
+ '/i/move': {
/**
- * i/import-antennas
+ * i/move
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___import-antennas'];
+ post: operations['i___move'];
};
'/i/notifications': {
/**
@@ -2371,23 +2590,23 @@ export type paths = {
*/
post: operations['i___regenerate-token'];
};
- '/i/registry/get-all': {
+ '/i/registry/get': {
/**
- * i/registry/get-all
+ * i/registry/get
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['i___registry___get-all'];
+ post: operations['i___registry___get'];
};
- '/i/registry/get-unsecure': {
+ '/i/registry/get-all': {
/**
- * i/registry/get-unsecure
+ * i/registry/get-all
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['i___registry___get-unsecure'];
+ post: operations['i___registry___get-all'];
};
'/i/registry/get-detail': {
/**
@@ -2398,32 +2617,32 @@ export type paths = {
*/
post: operations['i___registry___get-detail'];
};
- '/i/registry/get': {
+ '/i/registry/get-unsecure': {
/**
- * i/registry/get
+ * i/registry/get-unsecure
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['i___registry___get'];
+ post: operations['i___registry___get-unsecure'];
};
- '/i/registry/keys-with-type': {
+ '/i/registry/keys': {
/**
- * i/registry/keys-with-type
+ * i/registry/keys
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['i___registry___keys-with-type'];
+ post: operations['i___registry___keys'];
};
- '/i/registry/keys': {
+ '/i/registry/keys-with-type': {
/**
- * i/registry/keys
+ * i/registry/keys-with-type
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['i___registry___keys'];
+ post: operations['i___registry___keys-with-type'];
};
'/i/registry/remove': {
/**
@@ -2482,16 +2701,6 @@ export type paths = {
*/
post: operations['i___unpin'];
};
- '/i/update-email': {
- /**
- * i/update-email
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
- */
- post: operations['i___update-email'];
- };
'/i/update': {
/**
* i/update
@@ -2501,15 +2710,15 @@ export type paths = {
*/
post: operations['i___update'];
};
- '/i/move': {
+ '/i/update-email': {
/**
- * i/move
+ * i/update-email
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['i___move'];
+ post: operations['i___update-email'];
};
'/i/webhooks/create': {
/**
@@ -2520,6 +2729,15 @@ export type paths = {
*/
post: operations['i___webhooks___create'];
};
+ '/i/webhooks/delete': {
+ /**
+ * i/webhooks/delete
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:account*
+ */
+ post: operations['i___webhooks___delete'];
+ };
'/i/webhooks/list': {
/**
* i/webhooks/list
@@ -2538,33 +2756,24 @@ export type paths = {
*/
post: operations['i___webhooks___show'];
};
- '/i/webhooks/update': {
+ '/i/webhooks/test': {
/**
- * i/webhooks/update
+ * i/webhooks/test
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['i___webhooks___update'];
+ post: operations['i___webhooks___test'];
};
- '/i/webhooks/delete': {
+ '/i/webhooks/update': {
/**
- * i/webhooks/delete
+ * i/webhooks/update
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- post: operations['i___webhooks___delete'];
- };
- '/i/webhooks/test': {
- /**
- * i/webhooks/test
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *read:account*
- */
- post: operations['i___webhooks___test'];
+ post: operations['i___webhooks___update'];
};
'/invite/create': {
/**
@@ -2584,23 +2793,23 @@ export type paths = {
*/
post: operations['invite___delete'];
};
- '/invite/list': {
+ '/invite/limit': {
/**
- * invite/list
+ * invite/limit
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:invite-codes*
*/
- post: operations['invite___list'];
+ post: operations['invite___limit'];
};
- '/invite/limit': {
+ '/invite/list': {
/**
- * invite/limit
+ * invite/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:invite-codes*
*/
- post: operations['invite___limit'];
+ post: operations['invite___list'];
};
'/meta': {
/**
@@ -2611,38 +2820,6 @@ export type paths = {
*/
post: operations['meta'];
};
- '/emojis': {
- /**
- * emojis
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- get: operations['emojis'];
- /**
- * emojis
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['emojis'];
- };
- '/emoji': {
- /**
- * emoji
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- get: operations['emoji'];
- /**
- * emoji
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['emoji'];
- };
'/miauth/gen-token': {
/**
* miauth/gen-token
@@ -2680,33 +2857,6 @@ export type paths = {
*/
post: operations['mute___list'];
};
- '/renote-mute/create': {
- /**
- * renote-mute/create
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:mutes*
- */
- post: operations['renote-mute___create'];
- };
- '/renote-mute/delete': {
- /**
- * renote-mute/delete
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:mutes*
- */
- post: operations['renote-mute___delete'];
- };
- '/renote-mute/list': {
- /**
- * renote-mute/list
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *read:mutes*
- */
- post: operations['renote-mute___list'];
- };
'/my/apps': {
/**
* my/apps
@@ -2725,6 +2875,15 @@ export type paths = {
*/
post: operations['notes'];
};
+ '/notes/bubble-timeline': {
+ /**
+ * notes/bubble-timeline
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['notes___bubble-timeline'];
+ };
'/notes/children': {
/**
* notes/children
@@ -2770,6 +2929,15 @@ export type paths = {
*/
post: operations['notes___delete'];
};
+ '/notes/edit': {
+ /**
+ * notes/edit
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
+ */
+ post: operations['notes___edit'];
+ };
'/notes/favorites/create': {
/**
* notes/favorites/create
@@ -2829,23 +2997,23 @@ export type paths = {
*/
post: operations['notes___global-timeline'];
};
- '/notes/bubble-timeline': {
+ '/notes/hybrid-timeline': {
/**
- * notes/bubble-timeline
+ * notes/hybrid-timeline
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['notes___bubble-timeline'];
+ post: operations['notes___hybrid-timeline'];
};
- '/notes/hybrid-timeline': {
+ '/notes/like': {
/**
- * notes/hybrid-timeline
+ * notes/like
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:reactions*
*/
- post: operations['notes___hybrid-timeline'];
+ post: operations['notes___like'];
};
'/notes/local-timeline': {
/**
@@ -2874,23 +3042,23 @@ export type paths = {
*/
post: operations['notes___polls___recommendation'];
};
- '/notes/polls/vote': {
+ '/notes/polls/refresh': {
/**
- * notes/polls/vote
+ * notes/polls/refresh
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:votes*
+ * **Credential required**: *Yes* / **Permission**: *read:federation*
*/
- post: operations['notes___polls___vote'];
+ post: operations['notes___polls___refresh'];
};
- '/notes/polls/refresh': {
+ '/notes/polls/vote': {
/**
- * notes/polls/refresh
+ * notes/polls/vote
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:federation*
+ * **Credential required**: *Yes* / **Permission**: *write:votes*
*/
- post: operations['notes___polls___refresh'];
+ post: operations['notes___polls___vote'];
};
'/notes/reactions': {
/**
@@ -2926,15 +3094,6 @@ export type paths = {
*/
post: operations['notes___reactions___delete'];
};
- '/notes/like': {
- /**
- * notes/like
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:reactions*
- */
- post: operations['notes___like'];
- };
'/notes/renotes': {
/**
* notes/renotes
@@ -2980,23 +3139,23 @@ export type paths = {
*/
post: operations['notes___schedule___list'];
};
- '/notes/search-by-tag': {
+ '/notes/search': {
/**
- * notes/search-by-tag
+ * notes/search
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['notes___search-by-tag'];
+ post: operations['notes___search'];
};
- '/notes/search': {
+ '/notes/search-by-tag': {
/**
- * notes/search
+ * notes/search-by-tag
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['notes___search'];
+ post: operations['notes___search-by-tag'];
};
'/notes/show': {
/**
@@ -3070,15 +3229,6 @@ export type paths = {
*/
post: operations['notes___user-list-timeline'];
};
- '/notes/edit': {
- /**
- * notes/edit
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:notes*
- */
- post: operations['notes___edit'];
- };
'/notes/versions': {
/**
* notes/versions
@@ -3197,176 +3347,201 @@ export type paths = {
*/
post: operations['pages___update'];
};
- '/flash/create': {
+ '/ping': {
/**
- * flash/create
+ * ping
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *No*
*/
- post: operations['flash___create'];
+ post: operations['ping'];
};
- '/flash/delete': {
+ '/pinned-users': {
/**
- * flash/delete
+ * pinned-users
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *No*
*/
- post: operations['flash___delete'];
+ post: operations['pinned-users'];
};
- '/flash/featured': {
+ '/promo/read': {
/**
- * flash/featured
+ * promo/read
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- post: operations['flash___featured'];
+ post: operations['promo___read'];
};
- '/flash/like': {
+ '/renote-mute/create': {
/**
- * flash/like
+ * renote-mute/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ * **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
- post: operations['flash___like'];
+ post: operations['renote-mute___create'];
};
- '/flash/show': {
+ '/renote-mute/delete': {
/**
- * flash/show
+ * renote-mute/delete
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
- post: operations['flash___show'];
+ post: operations['renote-mute___delete'];
};
- '/flash/unlike': {
+ '/renote-mute/list': {
/**
- * flash/unlike
+ * renote-mute/list
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ * **Credential required**: *Yes* / **Permission**: *read:mutes*
*/
- post: operations['flash___unlike'];
+ post: operations['renote-mute___list'];
};
- '/flash/update': {
+ '/request-reset-password': {
/**
- * flash/update
- * @description No description provided.
+ * request-reset-password
+ * @description Request a users password to be reset.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *No*
*/
- post: operations['flash___update'];
+ post: operations['request-reset-password'];
};
- '/flash/my': {
+ '/reset-db': {
/**
- * flash/my
- * @description No description provided.
+ * reset-db
+ * @description Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis.
*
- * **Credential required**: *Yes* / **Permission**: *read:flash*
+ * **Credential required**: *No*
*/
- post: operations['flash___my'];
+ post: operations['reset-db'];
};
- '/flash/my-likes': {
+ '/reset-password': {
/**
- * flash/my-likes
- * @description No description provided.
+ * reset-password
+ * @description Complete the password reset that was previously requested.
*
- * **Credential required**: *Yes* / **Permission**: *read:flash-likes*
+ * **Credential required**: *No*
*/
- post: operations['flash___my-likes'];
+ post: operations['reset-password'];
};
- '/ping': {
+ '/retention': {
/**
- * ping
+ * retention
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['ping'];
- };
- '/pinned-users': {
+ get: operations['retention'];
/**
- * pinned-users
+ * retention
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['pinned-users'];
+ post: operations['retention'];
};
- '/promo/read': {
+ '/reversi/cancel-match': {
/**
- * promo/read
+ * reversi/cancel-match
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- post: operations['promo___read'];
+ post: operations['reversi___cancel-match'];
};
- '/roles/list': {
+ '/reversi/games': {
/**
- * roles/list
+ * reversi/games
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['reversi___games'];
+ };
+ '/reversi/invitations': {
+ /**
+ * reversi/invitations
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['roles___list'];
+ post: operations['reversi___invitations'];
};
- '/roles/show': {
+ '/reversi/match': {
/**
- * roles/show
+ * reversi/match
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:account*
+ */
+ post: operations['reversi___match'];
+ };
+ '/reversi/show-game': {
+ /**
+ * reversi/show-game
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['roles___show'];
+ post: operations['reversi___show-game'];
};
- '/roles/users': {
+ '/reversi/surrender': {
/**
- * roles/users
+ * reversi/surrender
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:account*
+ */
+ post: operations['reversi___surrender'];
+ };
+ '/reversi/verify': {
+ /**
+ * reversi/verify
* @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['roles___users'];
+ post: operations['reversi___verify'];
};
- '/roles/notes': {
+ '/roles/list': {
/**
- * roles/notes
+ * roles/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['roles___notes'];
+ post: operations['roles___list'];
};
- '/request-reset-password': {
+ '/roles/notes': {
/**
- * request-reset-password
- * @description Request a users password to be reset.
+ * roles/notes
+ * @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['request-reset-password'];
+ post: operations['roles___notes'];
};
- '/reset-db': {
+ '/roles/show': {
/**
- * reset-db
- * @description Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis.
+ * roles/show
+ * @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['reset-db'];
+ post: operations['roles___show'];
};
- '/reset-password': {
+ '/roles/users': {
/**
- * reset-password
- * @description Complete the password reset that was previously requested.
+ * roles/users
+ * @description No description provided.
*
* **Credential required**: *No*
*/
- post: operations['reset-password'];
+ post: operations['roles___users'];
};
'/server-info': {
/**
@@ -3384,44 +3559,43 @@ export type paths = {
*/
post: operations['server-info'];
};
- '/stats': {
+ '/sponsors': {
/**
- * stats
- * @description No description provided.
+ * sponsors
+ * @description Get Sharkey Sponsors or Instance Sponsors
*
* **Credential required**: *No*
*/
- post: operations['stats'];
+ post: operations['sponsors'];
};
- '/sw/show-registration': {
+ '/stats': {
/**
- * sw/show-registration
- * @description Check push notification registration exists.
+ * stats
+ * @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
+ * **Credential required**: *No*
*/
- post: operations['sw___show-registration'];
+ post: operations['stats'];
};
- '/sw/update-registration': {
+ '/sw/register': {
/**
- * sw/update-registration
- * @description Update push notification registration.
+ * sw/register
+ * @description Register to receive push notifications.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['sw___update-registration'];
+ post: operations['sw___register'];
};
- '/sw/register': {
+ '/sw/show-registration': {
/**
- * sw/register
- * @description Register to receive push notifications.
+ * sw/show-registration
+ * @description Check push notification registration exists.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- post: operations['sw___register'];
+ post: operations['sw___show-registration'];
};
'/sw/unregister': {
/**
@@ -3432,6 +3606,16 @@ export type paths = {
*/
post: operations['sw___unregister'];
};
+ '/sw/update-registration': {
+ /**
+ * sw/update-registration
+ * @description Update push notification registration.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
+ */
+ post: operations['sw___update-registration'];
+ };
'/test': {
/**
* test
@@ -3459,6 +3643,15 @@ export type paths = {
*/
post: operations['users'];
};
+ '/users/achievements': {
+ /**
+ * users/achievements
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['users___achievements'];
+ };
'/users/clips': {
/**
* users/clips
@@ -3468,6 +3661,31 @@ export type paths = {
*/
post: operations['users___clips'];
};
+ '/users/featured-notes': {
+ /**
+ * users/featured-notes
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ get: operations['users___featured-notes'];
+ /**
+ * users/featured-notes
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['users___featured-notes'];
+ };
+ '/users/flashs': {
+ /**
+ * users/flashs
+ * @description Show all flashs this user created.
+ *
+ * **Credential required**: *No*
+ */
+ post: operations['users___flashs'];
+ };
'/users/followers': {
/**
* users/followers
@@ -3504,22 +3722,6 @@ export type paths = {
*/
post: operations['users___get-frequently-replied-users'];
};
- '/users/featured-notes': {
- /**
- * users/featured-notes
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- get: operations['users___featured-notes'];
- /**
- * users/featured-notes
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['users___featured-notes'];
- };
'/users/lists/create': {
/**
* users/lists/create
@@ -3529,6 +3731,15 @@ export type paths = {
*/
post: operations['users___lists___create'];
};
+ '/users/lists/create-from-public': {
+ /**
+ * users/lists/create-from-public
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:account*
+ */
+ post: operations['users___lists___create-from-public'];
+ };
'/users/lists/delete': {
/**
* users/lists/delete
@@ -3538,6 +3749,24 @@ export type paths = {
*/
post: operations['users___lists___delete'];
};
+ '/users/lists/favorite': {
+ /**
+ * users/lists/favorite
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:account*
+ */
+ post: operations['users___lists___favorite'];
+ };
+ '/users/lists/get-memberships': {
+ /**
+ * users/lists/get-memberships
+ * @description No description provided.
+ *
+ * **Credential required**: *No* / **Permission**: *read:account*
+ */
+ post: operations['users___lists___get-memberships'];
+ };
'/users/lists/list': {
/**
* users/lists/list
@@ -3574,15 +3803,6 @@ export type paths = {
*/
post: operations['users___lists___show'];
};
- '/users/lists/favorite': {
- /**
- * users/lists/favorite
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- post: operations['users___lists___favorite'];
- };
'/users/lists/unfavorite': {
/**
* users/lists/unfavorite
@@ -3601,15 +3821,6 @@ export type paths = {
*/
post: operations['users___lists___update'];
};
- '/users/lists/create-from-public': {
- /**
- * users/lists/create-from-public
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- post: operations['users___lists___create-from-public'];
- };
'/users/lists/update-membership': {
/**
* users/lists/update-membership
@@ -3619,15 +3830,6 @@ export type paths = {
*/
post: operations['users___lists___update-membership'];
};
- '/users/lists/get-memberships': {
- /**
- * users/lists/get-memberships
- * @description No description provided.
- *
- * **Credential required**: *No* / **Permission**: *read:account*
- */
- post: operations['users___lists___get-memberships'];
- };
'/users/notes': {
/**
* users/notes
@@ -3646,15 +3848,6 @@ export type paths = {
*/
post: operations['users___pages'];
};
- '/users/flashs': {
- /**
- * users/flashs
- * @description Show all flashs this user created.
- *
- * **Credential required**: *No*
- */
- post: operations['users___flashs'];
- };
'/users/reactions': {
/**
* users/reactions
@@ -3691,15 +3884,6 @@ export type paths = {
*/
post: operations['users___report-abuse'];
};
- '/users/search-by-username-and-host': {
- /**
- * users/search-by-username-and-host
- * @description Search for a user by username and/or host.
- *
- * **Credential required**: *No*
- */
- post: operations['users___search-by-username-and-host'];
- };
'/users/search': {
/**
* users/search
@@ -3709,23 +3893,23 @@ export type paths = {
*/
post: operations['users___search'];
};
- '/users/show': {
+ '/users/search-by-username-and-host': {
/**
- * users/show
- * @description Show the properties of a user.
+ * users/search-by-username-and-host
+ * @description Search for a user by username and/or host.
*
* **Credential required**: *No*
*/
- post: operations['users___show'];
+ post: operations['users___search-by-username-and-host'];
};
- '/users/achievements': {
+ '/users/show': {
/**
- * users/achievements
- * @description No description provided.
+ * users/show
+ * @description Show the properties of a user.
*
* **Credential required**: *No*
*/
- post: operations['users___achievements'];
+ post: operations['users___show'];
};
'/users/update-memo': {
/**
@@ -3736,144 +3920,14 @@ export type paths = {
*/
post: operations['users___update-memo'];
};
- '/fetch-rss': {
- /**
- * fetch-rss
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- get: operations['fetch-rss'];
- /**
- * fetch-rss
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['fetch-rss'];
- };
- '/fetch-external-resources': {
- /**
- * fetch-external-resources
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
- */
- post: operations['fetch-external-resources'];
- };
- '/retention': {
- /**
- * retention
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- get: operations['retention'];
- /**
- * retention
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['retention'];
- };
- '/sponsors': {
- /**
- * sponsors
- * @description Get Sharkey Sponsors or Instance Sponsors
- *
- * **Credential required**: *No*
- */
- post: operations['sponsors'];
- };
- '/bubble-game/register': {
+ '/v2/admin/emoji/list': {
/**
- * bubble-game/register
+ * v2/admin/emoji/list
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- post: operations['bubble-game___register'];
- };
- '/bubble-game/ranking': {
- /**
- * bubble-game/ranking
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- get: operations['bubble-game___ranking'];
- /**
- * bubble-game/ranking
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['bubble-game___ranking'];
- };
- '/reversi/cancel-match': {
- /**
- * reversi/cancel-match
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- post: operations['reversi___cancel-match'];
- };
- '/reversi/games': {
- /**
- * reversi/games
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['reversi___games'];
- };
- '/reversi/match': {
- /**
- * reversi/match
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- post: operations['reversi___match'];
- };
- '/reversi/invitations': {
- /**
- * reversi/invitations
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *read:account*
- */
- post: operations['reversi___invitations'];
- };
- '/reversi/show-game': {
- /**
- * reversi/show-game
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['reversi___show-game'];
- };
- '/reversi/surrender': {
- /**
- * reversi/surrender
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- post: operations['reversi___surrender'];
- };
- '/reversi/verify': {
- /**
- * reversi/verify
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- post: operations['reversi___verify'];
+ post: operations['v2___admin___emoji___list'];
};
};
@@ -3932,6 +3986,8 @@ export type components = {
isSystem?: boolean;
noindex: boolean;
enableRss: boolean;
+ mandatoryCW: string | null;
+ rejectQuotes?: boolean;
isBot?: boolean;
isCat?: boolean;
speakAsCat?: boolean;
@@ -4217,6 +4273,9 @@ export type components = {
/** Format: date-time */
lastUsed: string;
}[];
+ defaultCW: string | null;
+ /** @enum {string} */
+ defaultCWPriority: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
};
UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'];
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
@@ -4291,6 +4350,8 @@ export type components = {
/** Format: date-time */
createdAt: string;
/** Format: date-time */
+ updatedAt?: string;
+ /** Format: date-time */
deletedAt?: string | null;
text: string | null;
cw?: string | null;
@@ -4359,6 +4420,7 @@ export type components = {
url?: string;
reactionAndUserPairCache?: string[];
clippedCount?: number;
+ processErrors?: string[] | null;
myReaction?: string | null;
};
NoteReaction: {
@@ -4916,6 +4978,7 @@ export type components = {
latestRequestReceivedAt: string | null;
isNSFW: boolean;
rejectReports: boolean;
+ rejectQuotes: boolean;
moderationNote?: string | null;
};
GalleryPost: {
@@ -4963,6 +5026,29 @@ export type components = {
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
};
+ EmojiDetailedAdmin: {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ updatedAt: string | null;
+ name: string;
+ /** @description The local host is represented with `null`. */
+ host: string | null;
+ publicUrl: string;
+ originalUrl: string;
+ uri: string | null;
+ type: string | null;
+ aliases: string[];
+ category: string | null;
+ license: string | null;
+ localOnly: boolean;
+ isSensitive: boolean;
+ roleIdsThatCanBeUsedThisEmojiAsReaction: {
+ /** Format: misskey:id */
+ id: string;
+ name: string;
+ }[];
+ };
Flash: {
/**
* Format: id
@@ -5224,6 +5310,7 @@ export type components = {
enableFC: boolean;
fcSiteKey: string | null;
enableAchievements: boolean | null;
+ robotsTxt: string | null;
enableTestcaptcha: boolean;
swPublickey: string | null;
/** @default /assets/ai.png */
@@ -5276,6 +5363,8 @@ export type components = {
noteSearchableScope: 'local' | 'global';
trustedLinkUrlPatterns: string[];
maxFileSize: number;
+ /** @enum {string} */
+ federation: 'all' | 'specified' | 'none';
};
MetaDetailedOnly: {
features?: {
@@ -5339,150 +5428,32 @@ export type external = Record<string, never>;
export type operations = {
/**
- * admin/meta
+ * admin/abuse-report/notification-recipient/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
*/
- admin___meta: {
+ 'admin___abuse-report___notification-recipient___create': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ isActive: boolean;
+ name: string;
+ /** @enum {string} */
+ method: 'email' | 'webhook';
+ /** Format: misskey:id */
+ userId?: string;
+ /** Format: misskey:id */
+ systemWebhookId?: string;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- cacheRemoteFiles: boolean;
- cacheRemoteSensitiveFiles: boolean;
- emailRequiredForSignup: boolean;
- approvalRequiredForSignup: boolean;
- enableHcaptcha: boolean;
- hcaptchaSiteKey: string | null;
- enableMcaptcha: boolean;
- mcaptchaSiteKey: string | null;
- mcaptchaInstanceUrl: string | null;
- enableRecaptcha: boolean;
- recaptchaSiteKey: string | null;
- enableTurnstile: boolean;
- turnstileSiteKey: string | null;
- enableFC: boolean;
- fcSiteKey: string | null;
- enableTestcaptcha: boolean;
- swPublickey: string | null;
- /** @default /assets/ai.png */
- mascotImageUrl: string | null;
- bannerUrl: string | null;
- serverErrorImageUrl: string | null;
- infoImageUrl: string | null;
- notFoundImageUrl: string | null;
- iconUrl: string | null;
- app192IconUrl: string | null;
- app512IconUrl: string | null;
- sidebarLogoUrl: string | null;
- enableEmail: boolean;
- enableServiceWorker: boolean;
- translatorAvailable: boolean;
- silencedHosts: string[];
- mediaSilencedHosts: string[];
- pinnedUsers: string[];
- hiddenTags: string[];
- blockedHosts: string[];
- sensitiveWords: string[];
- prohibitedWords: string[];
- prohibitedWordsForNameOfUser: string[];
- bannedEmailDomains?: string[];
- preservedUsernames: string[];
- bubbleInstances: string[];
- hcaptchaSecretKey: string | null;
- mcaptchaSecretKey: string | null;
- recaptchaSecretKey: string | null;
- turnstileSecretKey: string | null;
- fcSecretKey: string | null;
- sensitiveMediaDetection: string;
- sensitiveMediaDetectionSensitivity: string;
- setSensitiveFlagAutomatically: boolean;
- enableSensitiveMediaDetectionForVideos: boolean;
- enableBotTrending: boolean;
- /** Format: id */
- proxyAccountId: string | null;
- email: string | null;
- smtpSecure: boolean;
- smtpHost: string | null;
- smtpPort: number | null;
- smtpUser: string | null;
- smtpPass: string | null;
- swPrivateKey: string | null;
- useObjectStorage: boolean;
- objectStorageBaseUrl: string | null;
- objectStorageBucket: string | null;
- objectStoragePrefix: string | null;
- objectStorageEndpoint: string | null;
- objectStorageRegion: string | null;
- objectStoragePort: number | null;
- objectStorageAccessKey: string | null;
- objectStorageSecretKey: string | null;
- objectStorageUseSSL: boolean;
- objectStorageUseProxy: boolean;
- objectStorageSetPublicRead: boolean;
- enableIpLogging: boolean;
- enableActiveEmailValidation: boolean;
- enableVerifymailApi: boolean;
- verifymailAuthKey: string | null;
- enableTruemailApi: boolean;
- truemailInstance: string | null;
- truemailAuthKey: string | null;
- enableChartsForRemoteUser: boolean;
- enableChartsForFederatedInstances: boolean;
- enableStatsForFederatedInstances: boolean;
- enableServerMachineStats: boolean;
- enableAchievements: boolean;
- enableIdenticonGeneration: boolean;
- manifestJsonOverride: string;
- policies: Record<string, never>;
- enableFanoutTimeline: boolean;
- enableFanoutTimelineDbFallback: boolean;
- perLocalUserUserTimelineCacheMax: number;
- perRemoteUserUserTimelineCacheMax: number;
- perUserHomeTimelineCacheMax: number;
- perUserListTimelineCacheMax: number;
- enableReactionsBuffering: boolean;
- notesPerOneAd: number;
- backgroundImageUrl: string | null;
- deeplAuthKey: string | null;
- deeplIsPro: boolean;
- deeplFreeMode: boolean;
- deeplFreeInstance: string | null;
- defaultDarkTheme: string | null;
- defaultLightTheme: string | null;
- description: string | null;
- disableRegistration: boolean;
- impressumUrl: string | null;
- donationUrl: string | null;
- maintainerEmail: string | null;
- maintainerName: string | null;
- name: string | null;
- shortName: string | null;
- objectStorageS3ForcePathStyle: boolean;
- privacyPolicyUrl: string | null;
- inquiryUrl: string | null;
- repositoryUrl: string | null;
- /**
- * @deprecated
- * @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead.
- */
- summalyProxy: string | null;
- themeColor: string | null;
- tosUrl: string | null;
- uri: string;
- version: string;
- urlPreviewEnabled: boolean;
- urlPreviewTimeout: number;
- urlPreviewMaximumContentLength: number;
- urlPreviewRequireContentLength: boolean;
- urlPreviewUserAgent: string | null;
- urlPreviewSummaryProxyUrl: string | null;
- trustedLinkUrlPatterns: string[];
- federation: string;
- federationHosts: string[];
- };
+ 'application/json': components['schemas']['AbuseReportNotificationRecipient'];
};
};
/** @description Client error */
@@ -5518,66 +5489,25 @@ export type operations = {
};
};
/**
- * admin/abuse-user-reports
+ * admin/abuse-report/notification-recipient/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
*/
- 'admin___abuse-user-reports': {
+ 'admin___abuse-report___notification-recipient___delete': {
requestBody: {
content: {
'application/json': {
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
/** Format: misskey:id */
- untilId?: string;
- /** @default null */
- state?: string | null;
- /**
- * @default combined
- * @enum {string}
- */
- reporterOrigin?: 'combined' | 'local' | 'remote';
- /**
- * @default combined
- * @enum {string}
- */
- targetUserOrigin?: 'combined' | 'local' | 'remote';
+ id: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': ({
- /**
- * Format: id
- * @example xxxxxxxxxx
- */
- id: string;
- /** Format: date-time */
- createdAt: string;
- comment: string;
- /** @example false */
- resolved: boolean;
- /** Format: id */
- reporterId: string;
- /** Format: id */
- targetUserId: string;
- /** Format: id */
- assigneeId: string | null;
- reporter: components['schemas']['UserDetailedNotMe'];
- targetUser: components['schemas']['UserDetailedNotMe'];
- assignee: components['schemas']['UserDetailedNotMe'] | null;
- forwarded: boolean;
- /** @enum {string|null} */
- resolvedAs: 'accept' | 'reject' | null;
- moderationNote: string;
- })[];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -5721,16 +5651,18 @@ export type operations = {
};
};
/**
- * admin/abuse-report/notification-recipient/create
+ * admin/abuse-report/notification-recipient/update
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
*/
- 'admin___abuse-report___notification-recipient___create': {
+ 'admin___abuse-report___notification-recipient___update': {
requestBody: {
content: {
'application/json': {
+ /** Format: misskey:id */
+ id: string;
isActive: boolean;
name: string;
/** @enum {string} */
@@ -5782,26 +5714,33 @@ export type operations = {
};
};
/**
- * admin/abuse-report/notification-recipient/update
+ * admin/abuse-user-reports
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports*
*/
- 'admin___abuse-report___notification-recipient___update': {
+ 'admin___abuse-user-reports': {
requestBody: {
content: {
'application/json': {
+ /** @default 10 */
+ limit?: number;
/** Format: misskey:id */
- id: string;
- isActive: boolean;
- name: string;
- /** @enum {string} */
- method: 'email' | 'webhook';
- /** Format: misskey:id */
- userId?: string;
+ sinceId?: string;
/** Format: misskey:id */
- systemWebhookId?: string;
+ untilId?: string;
+ /** @default null */
+ state?: string | null;
+ /**
+ * @default combined
+ * @enum {string}
+ */
+ reporterOrigin?: 'combined' | 'local' | 'remote';
+ /**
+ * @default combined
+ * @enum {string}
+ */
+ targetUserOrigin?: 'combined' | 'local' | 'remote';
};
};
};
@@ -5809,62 +5748,33 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['AbuseReportNotificationRecipient'];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * admin/abuse-report/notification-recipient/delete
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
- */
- 'admin___abuse-report___notification-recipient___delete': {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- id: string;
+ 'application/json': ({
+ /**
+ * Format: id
+ * @example xxxxxxxxxx
+ */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ comment: string;
+ /** @example false */
+ resolved: boolean;
+ /** Format: id */
+ reporterId: string;
+ /** Format: id */
+ targetUserId: string;
+ /** Format: id */
+ assigneeId: string | null;
+ reporter: components['schemas']['UserDetailedNotMe'];
+ targetUser: components['schemas']['UserDetailedNotMe'];
+ assignee: components['schemas']['UserDetailedNotMe'] | null;
+ forwarded: boolean;
+ /** @enum {string|null} */
+ resolvedAs: 'accept' | 'reject' | null;
+ moderationNote: string;
+ })[];
};
};
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
/** @description Client error */
400: {
content: {
@@ -6575,6 +6485,58 @@ export type operations = {
};
};
/**
+ * admin/approve-user
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
+ */
+ 'admin___approve-user': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ userId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* admin/avatar-decorations/create
* @description No description provided.
*
@@ -6824,17 +6786,144 @@ export type operations = {
};
};
/**
- * admin/delete-all-files-of-a-user
+ * admin/captcha/current
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
*/
- 'admin___delete-all-files-of-a-user': {
+ admin___captcha___current: {
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ /** @enum {string} */
+ provider: 'none' | 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'fc' | 'testcaptcha';
+ hcaptcha: {
+ siteKey: string | null;
+ secretKey: string | null;
+ };
+ mcaptcha: {
+ siteKey: string | null;
+ secretKey: string | null;
+ instanceUrl: string | null;
+ };
+ recaptcha: {
+ siteKey: string | null;
+ secretKey: string | null;
+ };
+ turnstile: {
+ siteKey: string | null;
+ secretKey: string | null;
+ };
+ fc: {
+ siteKey: string | null;
+ secretKey: string | null;
+ };
+ };
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/captcha/save
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
+ */
+ admin___captcha___save: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @enum {string} */
+ provider: 'none' | 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'fc' | 'testcaptcha';
+ captchaResult?: string | null;
+ sitekey?: string | null;
+ secret?: string | null;
+ instanceUrl?: string | null;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/cw-user
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:cw-user*
+ */
+ 'admin___cw-user': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
+ cw: string | null;
};
};
};
@@ -6876,12 +6965,12 @@ export type operations = {
};
};
/**
- * admin/unset-user-avatar
+ * admin/decline-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
*/
- 'admin___unset-user-avatar': {
+ 'admin___decline-user': {
requestBody: {
content: {
'application/json': {
@@ -6928,12 +7017,64 @@ export type operations = {
};
};
/**
- * admin/unset-user-banner
+ * admin/delete-account
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account*
*/
- 'admin___unset-user-banner': {
+ 'admin___delete-account': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ userId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/delete-all-files-of-a-user
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user*
+ */
+ 'admin___delete-all-files-of-a-user': {
requestBody: {
content: {
'application/json': {
@@ -7248,24 +7389,34 @@ export type operations = {
};
};
/**
- * admin/emoji/add-aliases-bulk
+ * admin/emoji/add
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- 'admin___emoji___add-aliases-bulk': {
+ admin___emoji___add: {
requestBody: {
content: {
'application/json': {
- ids: string[];
- aliases: string[];
+ name: string;
+ /** Format: misskey:id */
+ fileId: string;
+ /** @description Use `null` to reset the category. */
+ category?: string | null;
+ aliases?: string[];
+ license?: string | null;
+ isSensitive?: boolean;
+ localOnly?: boolean;
+ roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['EmojiDetailed'];
+ };
};
/** @description Client error */
400: {
@@ -7300,34 +7451,24 @@ export type operations = {
};
};
/**
- * admin/emoji/add
+ * admin/emoji/add-aliases-bulk
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- admin___emoji___add: {
+ 'admin___emoji___add-aliases-bulk': {
requestBody: {
content: {
'application/json': {
- name: string;
- /** Format: misskey:id */
- fileId: string;
- /** @description Use `null` to reset the category. */
- category?: string | null;
- aliases?: string[];
- license?: string | null;
- isSensitive?: boolean;
- localOnly?: boolean;
- roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
+ ids: string[];
+ aliases: string[];
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['EmojiDetailed'];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -7419,16 +7560,17 @@ export type operations = {
};
};
/**
- * admin/emoji/delete-bulk
+ * admin/emoji/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- 'admin___emoji___delete-bulk': {
+ admin___emoji___delete: {
requestBody: {
content: {
'application/json': {
- ids: string[];
+ /** Format: misskey:id */
+ id: string;
};
};
};
@@ -7470,17 +7612,16 @@ export type operations = {
};
};
/**
- * admin/emoji/delete
+ * admin/emoji/delete-bulk
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:emoji*
*/
- admin___emoji___delete: {
+ 'admin___emoji___delete-bulk': {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- id: string;
+ ids: string[];
};
};
};
@@ -7575,24 +7716,21 @@ export type operations = {
};
};
/**
- * admin/emoji/list-remote
+ * admin/emoji/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- 'admin___emoji___list-remote': {
+ admin___emoji___list: {
requestBody: {
content: {
'application/json': {
/** @default null */
query?: string | null;
- /**
- * @description Use `null` to represent the local host.
- * @default null
- */
- host?: string | null;
/** @default 10 */
limit?: number;
+ /** @default null */
+ offset?: number | null;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
@@ -7610,7 +7748,7 @@ export type operations = {
aliases: string[];
name: string;
category: string | null;
- /** @description The local host is represented with `null`. */
+ /** @description The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files. */
host: string | null;
url: string;
})[];
@@ -7649,21 +7787,24 @@ export type operations = {
};
};
/**
- * admin/emoji/list
+ * admin/emoji/list-remote
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- admin___emoji___list: {
+ 'admin___emoji___list-remote': {
requestBody: {
content: {
'application/json': {
/** @default null */
query?: string | null;
+ /**
+ * @description Use `null` to represent the local host.
+ * @default null
+ */
+ host?: string | null;
/** @default 10 */
limit?: number;
- /** @default null */
- offset?: number | null;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
@@ -7681,7 +7822,7 @@ export type operations = {
aliases: string[];
name: string;
category: string | null;
- /** @description The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files. */
+ /** @description The local host is represented with `null`. */
host: string | null;
url: string;
})[];
@@ -8159,6 +8300,59 @@ export type operations = {
isNSFW?: boolean;
rejectReports?: boolean;
moderationNote?: string;
+ rejectQuotes?: boolean;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/forward-abuse-user-report
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ 'admin___forward-abuse-user-report': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ reportId: string;
};
};
};
@@ -8200,6 +8394,50 @@ export type operations = {
};
};
/**
+ * admin/gen-vapid-keys
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
+ */
+ 'admin___gen-vapid-keys': {
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* admin/get-index-stats
* @description No description provided.
*
@@ -8476,6 +8714,238 @@ export type operations = {
};
};
/**
+ * admin/meta
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:admin:meta*
+ */
+ admin___meta: {
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ cacheRemoteFiles: boolean;
+ cacheRemoteSensitiveFiles: boolean;
+ emailRequiredForSignup: boolean;
+ approvalRequiredForSignup: boolean;
+ enableHcaptcha: boolean;
+ hcaptchaSiteKey: string | null;
+ enableMcaptcha: boolean;
+ mcaptchaSiteKey: string | null;
+ mcaptchaInstanceUrl: string | null;
+ enableRecaptcha: boolean;
+ recaptchaSiteKey: string | null;
+ enableTurnstile: boolean;
+ turnstileSiteKey: string | null;
+ enableFC: boolean;
+ fcSiteKey: string | null;
+ enableTestcaptcha: boolean;
+ swPublickey: string | null;
+ /** @default /assets/ai.png */
+ mascotImageUrl: string | null;
+ bannerUrl: string | null;
+ serverErrorImageUrl: string | null;
+ infoImageUrl: string | null;
+ notFoundImageUrl: string | null;
+ iconUrl: string | null;
+ app192IconUrl: string | null;
+ app512IconUrl: string | null;
+ sidebarLogoUrl: string | null;
+ enableEmail: boolean;
+ enableServiceWorker: boolean;
+ translatorAvailable: boolean;
+ silencedHosts: string[];
+ mediaSilencedHosts: string[];
+ pinnedUsers: string[];
+ hiddenTags: string[];
+ blockedHosts: string[];
+ sensitiveWords: string[];
+ prohibitedWords: string[];
+ prohibitedWordsForNameOfUser: string[];
+ bannedEmailDomains?: string[];
+ preservedUsernames: string[];
+ bubbleInstances: string[];
+ hcaptchaSecretKey: string | null;
+ mcaptchaSecretKey: string | null;
+ recaptchaSecretKey: string | null;
+ turnstileSecretKey: string | null;
+ fcSecretKey: string | null;
+ sensitiveMediaDetection: string;
+ sensitiveMediaDetectionSensitivity: string;
+ setSensitiveFlagAutomatically: boolean;
+ enableSensitiveMediaDetectionForVideos: boolean;
+ enableBotTrending: boolean;
+ /** Format: id */
+ proxyAccountId: string | null;
+ email: string | null;
+ smtpSecure: boolean;
+ smtpHost: string | null;
+ smtpPort: number | null;
+ smtpUser: string | null;
+ smtpPass: string | null;
+ swPrivateKey: string | null;
+ useObjectStorage: boolean;
+ objectStorageBaseUrl: string | null;
+ objectStorageBucket: string | null;
+ objectStoragePrefix: string | null;
+ objectStorageEndpoint: string | null;
+ objectStorageRegion: string | null;
+ objectStoragePort: number | null;
+ objectStorageAccessKey: string | null;
+ objectStorageSecretKey: string | null;
+ objectStorageUseSSL: boolean;
+ objectStorageUseProxy: boolean;
+ objectStorageSetPublicRead: boolean;
+ enableIpLogging: boolean;
+ enableActiveEmailValidation: boolean;
+ enableVerifymailApi: boolean;
+ verifymailAuthKey: string | null;
+ enableTruemailApi: boolean;
+ truemailInstance: string | null;
+ truemailAuthKey: string | null;
+ enableChartsForRemoteUser: boolean;
+ enableChartsForFederatedInstances: boolean;
+ enableStatsForFederatedInstances: boolean;
+ enableServerMachineStats: boolean;
+ enableAchievements: boolean;
+ robotsTxt: string | null;
+ enableIdenticonGeneration: boolean;
+ manifestJsonOverride: string;
+ policies: Record<string, never>;
+ enableFanoutTimeline: boolean;
+ enableFanoutTimelineDbFallback: boolean;
+ perLocalUserUserTimelineCacheMax: number;
+ perRemoteUserUserTimelineCacheMax: number;
+ perUserHomeTimelineCacheMax: number;
+ perUserListTimelineCacheMax: number;
+ enableReactionsBuffering: boolean;
+ notesPerOneAd: number;
+ backgroundImageUrl: string | null;
+ deeplAuthKey: string | null;
+ deeplIsPro: boolean;
+ deeplFreeMode: boolean;
+ deeplFreeInstance: string | null;
+ defaultDarkTheme: string | null;
+ defaultLightTheme: string | null;
+ description: string | null;
+ disableRegistration: boolean;
+ impressumUrl: string | null;
+ donationUrl: string | null;
+ maintainerEmail: string | null;
+ maintainerName: string | null;
+ name: string | null;
+ shortName: string | null;
+ objectStorageS3ForcePathStyle: boolean;
+ privacyPolicyUrl: string | null;
+ inquiryUrl: string | null;
+ repositoryUrl: string | null;
+ /**
+ * @deprecated
+ * @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead.
+ */
+ summalyProxy: string | null;
+ themeColor: string | null;
+ tosUrl: string | null;
+ uri: string;
+ version: string;
+ urlPreviewEnabled: boolean;
+ urlPreviewTimeout: number;
+ urlPreviewMaximumContentLength: number;
+ urlPreviewRequireContentLength: boolean;
+ urlPreviewUserAgent: string | null;
+ urlPreviewSummaryProxyUrl: string | null;
+ trustedLinkUrlPatterns: string[];
+ federation: string;
+ federationHosts: string[];
+ };
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/nsfw-user
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:nsfw-user*
+ */
+ 'admin___nsfw-user': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ userId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* admin/promo/create
* @description No description provided.
*
@@ -8768,6 +9238,59 @@ export type operations = {
};
};
/**
+ * admin/reject-quotes
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:reject-quotes*
+ */
+ 'admin___reject-quotes': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ userId: string;
+ rejectQuotes: boolean;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* admin/relays/add
* @description No description provided.
*
@@ -9048,70 +9571,20 @@ export type operations = {
};
};
/**
- * admin/forward-abuse-user-report
+ * admin/roles/assign
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- 'admin___forward-abuse-user-report': {
+ admin___roles___assign: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- reportId: string;
- };
- };
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * admin/update-abuse-user-report
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
- */
- 'admin___update-abuse-user-report': {
- requestBody: {
- content: {
- 'application/json': {
+ roleId: string;
/** Format: misskey:id */
- reportId: string;
- moderationNote?: string;
+ userId: string;
+ expiresAt?: number | null;
};
};
};
@@ -9153,94 +9626,39 @@ export type operations = {
};
};
/**
- * admin/send-email
+ * admin/roles/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:send-email*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- 'admin___send-email': {
+ admin___roles___create: {
requestBody: {
content: {
'application/json': {
- to: string;
- subject: string;
- text: string;
- };
- };
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
+ name: string;
+ description: string;
+ color: string | null;
+ iconUrl: string | null;
+ /** @enum {string} */
+ target: 'manual' | 'conditional';
+ condFormula: Record<string, never>;
+ isPublic: boolean;
+ isModerator: boolean;
+ isAdministrator: boolean;
+ /** @default false */
+ isExplorable?: boolean;
+ asBadge: boolean;
+ canEditMembersByModerator: boolean;
+ displayOrder: number;
+ policies: Record<string, never>;
};
};
};
- };
- /**
- * admin/server-info
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *read:admin:server-info*
- */
- 'admin___server-info': {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- machine: string;
- /** @example linux */
- os: string;
- node: string;
- psql: string;
- cpu: {
- model: string;
- cores: number;
- };
- mem: {
- /** Format: bytes */
- total: number;
- };
- fs: {
- /** Format: bytes */
- total: number;
- /** Format: bytes */
- used: number;
- };
- net: {
- /** @example eth0 */
- interface: string;
- };
- };
+ 'application/json': components['schemas']['Role'];
};
};
/** @description Client error */
@@ -9276,43 +9694,24 @@ export type operations = {
};
};
/**
- * admin/show-moderation-logs
+ * admin/roles/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- 'admin___show-moderation-logs': {
+ admin___roles___delete: {
requestBody: {
content: {
'application/json': {
- /** @default 10 */
- limit?: number;
/** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- type?: string | null;
- /** Format: misskey:id */
- userId?: string | null;
+ roleId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- /** Format: id */
- id: string;
- /** Format: date-time */
- createdAt: string;
- type: string;
- info: Record<string, never>;
- /** Format: id */
- userId: string;
- user: components['schemas']['UserDetailedNotMe'];
- }[];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -9347,183 +9746,17 @@ export type operations = {
};
};
/**
- * admin/show-user
+ * admin/roles/list
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
*/
- 'admin___show-user': {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- userId: string;
- };
- };
- };
+ admin___roles___list: {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- email: string | null;
- emailVerified: boolean;
- approved: boolean;
- followedMessage: string | null;
- autoAcceptFollowed: boolean;
- noCrawle: boolean;
- preventAiLearning: boolean;
- alwaysMarkNsfw: boolean;
- autoSensitive: boolean;
- carefulBot: boolean;
- injectFeaturedNote: boolean;
- receiveAnnouncementEmail: boolean;
- mutedWords: (string | string[])[];
- mutedInstances: string[];
- notificationRecieveConfig: {
- note?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- follow?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- mention?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- reply?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- renote?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- quote?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- reaction?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- pollEnded?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- receiveFollowRequest?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- followRequestAccepted?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- roleAssigned?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- achievementEarned?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- app?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- test?: OneOf<[{
- /** @enum {string} */
- type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
- }, {
- /** @enum {string} */
- type: 'list';
- /** Format: misskey:id */
- userListId: string;
- }]>;
- };
- isModerator: boolean;
- isSystem: boolean;
- isSilenced: boolean;
- isSuspended: boolean;
- isHibernated: boolean;
- lastActiveDate: string | null;
- moderationNote: string;
- signins: components['schemas']['Signin'][];
- policies: components['schemas']['RolePolicies'];
- roles: components['schemas']['Role'][];
- roleAssigns: ({
- createdAt: string;
- expiresAt: string | null;
- roleId: string;
- })[];
- };
+ 'application/json': components['schemas']['Role'][];
};
};
/** @description Client error */
@@ -9559,38 +9792,17 @@ export type operations = {
};
};
/**
- * admin/show-users
+ * admin/roles/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
*/
- 'admin___show-users': {
+ admin___roles___show: {
requestBody: {
content: {
'application/json': {
- /** @default 10 */
- limit?: number;
- /** @default 0 */
- offset?: number;
- /** @enum {string} */
- sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt' | '+lastActiveDate' | '-lastActiveDate';
- /**
- * @default all
- * @enum {string}
- */
- state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended' | 'approved';
- /**
- * @default combined
- * @enum {string}
- */
- origin?: 'combined' | 'local' | 'remote';
- /** @default null */
- username?: string | null;
- /**
- * @description The local host is represented with `null`.
- * @default null
- */
- hostname?: string | null;
+ /** Format: misskey:id */
+ roleId: string;
};
};
};
@@ -9598,7 +9810,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['UserDetailed'][];
+ 'application/json': components['schemas']['Role'];
};
};
/** @description Client error */
@@ -9634,16 +9846,18 @@ export type operations = {
};
};
/**
- * admin/nsfw-user
+ * admin/roles/unassign
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:nsfw-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- 'admin___nsfw-user': {
+ admin___roles___unassign: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
+ roleId: string;
+ /** Format: misskey:id */
userId: string;
};
};
@@ -9686,17 +9900,32 @@ export type operations = {
};
};
/**
- * admin/unnsfw-user
+ * admin/roles/update
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unnsfw-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- 'admin___unnsfw-user': {
+ admin___roles___update: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- userId: string;
+ roleId: string;
+ name?: string;
+ description?: string;
+ color?: string | null;
+ iconUrl?: string | null;
+ /** @enum {string} */
+ target?: 'manual' | 'conditional';
+ condFormula?: Record<string, never>;
+ isPublic?: boolean;
+ isModerator?: boolean;
+ isAdministrator?: boolean;
+ isExplorable?: boolean;
+ asBadge?: boolean;
+ canEditMembersByModerator?: boolean;
+ displayOrder?: number;
+ policies?: Record<string, never>;
};
};
};
@@ -9738,17 +9967,16 @@ export type operations = {
};
};
/**
- * admin/silence-user
+ * admin/roles/update-default-policies
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:silence-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
*/
- 'admin___silence-user': {
+ 'admin___roles___update-default-policies': {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- userId: string;
+ policies: Record<string, never>;
};
};
};
@@ -9790,24 +10018,40 @@ export type operations = {
};
};
/**
- * admin/unsilence-user
+ * admin/roles/users
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unsilence-user*
+ * **Credential required**: *No* / **Permission**: *read:admin:roles*
*/
- 'admin___unsilence-user': {
+ admin___roles___users: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- userId: string;
+ roleId: string;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ /** @default 10 */
+ limit?: number;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': ({
+ /** Format: misskey:id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ user: components['schemas']['UserDetailed'];
+ /** Format: date-time */
+ expiresAt: string | null;
+ })[];
+ };
};
/** @description Client error */
400: {
@@ -9842,17 +10086,18 @@ export type operations = {
};
};
/**
- * admin/suspend-user
+ * admin/send-email
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:send-email*
*/
- 'admin___suspend-user': {
+ 'admin___send-email': {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- userId: string;
+ to: string;
+ subject: string;
+ text: string;
};
};
};
@@ -9894,24 +10139,42 @@ export type operations = {
};
};
/**
- * admin/approve-user
+ * admin/server-info
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:approve-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:server-info*
*/
- 'admin___approve-user': {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- userId: string;
- };
- };
- };
+ 'admin___server-info': {
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ machine: string;
+ /** @example linux */
+ os: string;
+ node: string;
+ psql: string;
+ cpu: {
+ model: string;
+ cores: number;
+ };
+ mem: {
+ /** Format: bytes */
+ total: number;
+ };
+ fs: {
+ /** Format: bytes */
+ total: number;
+ /** Format: bytes */
+ used: number;
+ };
+ net: {
+ /** @example eth0 */
+ interface: string;
+ };
+ };
+ };
};
/** @description Client error */
400: {
@@ -9946,24 +10209,43 @@ export type operations = {
};
};
/**
- * admin/decline-user
+ * admin/show-moderation-logs
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log*
*/
- 'admin___decline-user': {
+ 'admin___show-moderation-logs': {
requestBody: {
content: {
'application/json': {
+ /** @default 10 */
+ limit?: number;
/** Format: misskey:id */
- userId: string;
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ type?: string | null;
+ /** Format: misskey:id */
+ userId?: string | null;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ type: string;
+ info: Record<string, never>;
+ /** Format: id */
+ userId: string;
+ user: components['schemas']['UserDetailedNotMe'];
+ }[];
+ };
};
/** @description Client error */
400: {
@@ -9998,12 +10280,12 @@ export type operations = {
};
};
/**
- * admin/unsuspend-user
+ * admin/show-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/
- 'admin___unsuspend-user': {
+ 'admin___show-user': {
requestBody: {
content: {
'application/json': {
@@ -10013,9 +10295,169 @@ export type operations = {
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ email: string | null;
+ emailVerified: boolean;
+ approved: boolean;
+ followedMessage: string | null;
+ autoAcceptFollowed: boolean;
+ noCrawle: boolean;
+ preventAiLearning: boolean;
+ alwaysMarkNsfw: boolean;
+ autoSensitive: boolean;
+ carefulBot: boolean;
+ injectFeaturedNote: boolean;
+ receiveAnnouncementEmail: boolean;
+ mutedWords: (string | string[])[];
+ mutedInstances: string[];
+ notificationRecieveConfig: {
+ note?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ follow?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ mention?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ reply?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ renote?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ quote?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ reaction?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ pollEnded?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ receiveFollowRequest?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ followRequestAccepted?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ roleAssigned?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ achievementEarned?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ app?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ test?: OneOf<[{
+ /** @enum {string} */
+ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
+ }, {
+ /** @enum {string} */
+ type: 'list';
+ /** Format: misskey:id */
+ userListId: string;
+ }]>;
+ };
+ isModerator: boolean;
+ isSystem: boolean;
+ isSilenced: boolean;
+ isSuspended: boolean;
+ isHibernated: boolean;
+ lastActiveDate: string | null;
+ moderationNote: string;
+ signins: components['schemas']['Signin'][];
+ policies: components['schemas']['RolePolicies'];
+ roles: components['schemas']['Role'][];
+ roleAssigns: ({
+ createdAt: string;
+ expiresAt: string | null;
+ roleId: string;
+ })[];
+ };
+ };
};
/** @description Client error */
400: {
@@ -10050,154 +10492,47 @@ export type operations = {
};
};
/**
- * admin/update-meta
+ * admin/show-users
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/
- 'admin___update-meta': {
+ 'admin___show-users': {
requestBody: {
content: {
'application/json': {
- disableRegistration?: boolean | null;
- pinnedUsers?: string[] | null;
- hiddenTags?: string[] | null;
- blockedHosts?: string[] | null;
- sensitiveWords?: string[] | null;
- prohibitedWords?: string[] | null;
- prohibitedWordsForNameOfUser?: string[] | null;
- themeColor?: string | null;
- mascotImageUrl?: string | null;
- bannerUrl?: string | null;
- serverErrorImageUrl?: string | null;
- infoImageUrl?: string | null;
- notFoundImageUrl?: string | null;
- iconUrl?: string | null;
- app192IconUrl?: string | null;
- app512IconUrl?: string | null;
- sidebarLogoUrl?: string | null;
- backgroundImageUrl?: string | null;
- logoImageUrl?: string | null;
- name?: string | null;
- shortName?: string | null;
- description?: string | null;
- defaultLightTheme?: string | null;
- defaultDarkTheme?: string | null;
- defaultLike?: string | null;
- cacheRemoteFiles?: boolean;
- cacheRemoteSensitiveFiles?: boolean;
- emailRequiredForSignup?: boolean;
- approvalRequiredForSignup?: boolean;
- enableHcaptcha?: boolean;
- hcaptchaSiteKey?: string | null;
- hcaptchaSecretKey?: string | null;
- enableMcaptcha?: boolean;
- mcaptchaSiteKey?: string | null;
- mcaptchaInstanceUrl?: string | null;
- mcaptchaSecretKey?: string | null;
- enableRecaptcha?: boolean;
- recaptchaSiteKey?: string | null;
- recaptchaSecretKey?: string | null;
- enableTurnstile?: boolean;
- turnstileSiteKey?: string | null;
- turnstileSecretKey?: string | null;
- enableFC?: boolean;
- fcSiteKey?: string | null;
- fcSecretKey?: string | null;
- enableTestcaptcha?: boolean;
- /** @enum {string} */
- sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
- /** @enum {string} */
- sensitiveMediaDetectionSensitivity?: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh';
- setSensitiveFlagAutomatically?: boolean;
- enableSensitiveMediaDetectionForVideos?: boolean;
- enableBotTrending?: boolean;
- /** Format: misskey:id */
- proxyAccountId?: string | null;
- maintainerName?: string | null;
- maintainerEmail?: string | null;
- langs?: string[];
- deeplAuthKey?: string | null;
- deeplIsPro?: boolean;
- deeplFreeMode?: boolean;
- deeplFreeInstance?: string | null;
- enableEmail?: boolean;
- email?: string | null;
- smtpSecure?: boolean;
- smtpHost?: string | null;
- smtpPort?: number | null;
- smtpUser?: string | null;
- smtpPass?: string | null;
- enableServiceWorker?: boolean;
- swPublicKey?: string | null;
- swPrivateKey?: string | null;
- tosUrl?: string | null;
- repositoryUrl?: string | null;
- feedbackUrl?: string | null;
- impressumUrl?: string | null;
- donationUrl?: string | null;
- privacyPolicyUrl?: string | null;
- inquiryUrl?: string | null;
- useObjectStorage?: boolean;
- objectStorageBaseUrl?: string | null;
- objectStorageBucket?: string | null;
- objectStoragePrefix?: string | null;
- objectStorageEndpoint?: string | null;
- objectStorageRegion?: string | null;
- objectStoragePort?: number | null;
- objectStorageAccessKey?: string | null;
- objectStorageSecretKey?: string | null;
- objectStorageUseSSL?: boolean;
- objectStorageUseProxy?: boolean;
- objectStorageSetPublicRead?: boolean;
- objectStorageS3ForcePathStyle?: boolean;
- enableIpLogging?: boolean;
- enableActiveEmailValidation?: boolean;
- enableVerifymailApi?: boolean;
- verifymailAuthKey?: string | null;
- enableTruemailApi?: boolean;
- truemailInstance?: string | null;
- truemailAuthKey?: string | null;
- enableChartsForRemoteUser?: boolean;
- enableChartsForFederatedInstances?: boolean;
- enableStatsForFederatedInstances?: boolean;
- enableServerMachineStats?: boolean;
- enableAchievements?: boolean;
- enableIdenticonGeneration?: boolean;
- serverRules?: string[];
- bannedEmailDomains?: string[];
- preservedUsernames?: string[];
- bubbleInstances?: string[];
- manifestJsonOverride?: string;
- enableFanoutTimeline?: boolean;
- enableFanoutTimelineDbFallback?: boolean;
- perLocalUserUserTimelineCacheMax?: number;
- perRemoteUserUserTimelineCacheMax?: number;
- perUserHomeTimelineCacheMax?: number;
- perUserListTimelineCacheMax?: number;
- enableReactionsBuffering?: boolean;
- notesPerOneAd?: number;
- silencedHosts?: string[] | null;
- mediaSilencedHosts?: string[] | null;
- /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */
- summalyProxy?: string | null;
- urlPreviewEnabled?: boolean;
- urlPreviewTimeout?: number;
- urlPreviewMaximumContentLength?: number;
- urlPreviewRequireContentLength?: boolean;
- urlPreviewUserAgent?: string | null;
- urlPreviewSummaryProxyUrl?: string | null;
- trustedLinkUrlPatterns?: string[] | null;
+ /** @default 10 */
+ limit?: number;
+ /** @default 0 */
+ offset?: number;
/** @enum {string} */
- federation?: 'all' | 'none' | 'specified';
- federationHosts?: string[];
+ sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt' | '+lastActiveDate' | '-lastActiveDate';
+ /**
+ * @default all
+ * @enum {string}
+ */
+ state?: 'all' | 'alive' | 'available' | 'admin' | 'moderator' | 'adminOrModerator' | 'suspended' | 'approved';
+ /**
+ * @default combined
+ * @enum {string}
+ */
+ origin?: 'combined' | 'local' | 'remote';
+ /** @default null */
+ username?: string | null;
+ /**
+ * @description The local host is represented with `null`.
+ * @default null
+ */
+ hostname?: string | null;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['UserDetailed'][];
+ };
};
/** @description Client error */
400: {
@@ -10232,12 +10567,12 @@ export type operations = {
};
};
/**
- * admin/delete-account
+ * admin/silence-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:silence-user*
*/
- 'admin___delete-account': {
+ 'admin___silence-user': {
requestBody: {
content: {
'application/json': {
@@ -10284,18 +10619,17 @@ export type operations = {
};
};
/**
- * admin/update-user-note
+ * admin/suspend-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:user-note*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
*/
- 'admin___update-user-note': {
+ 'admin___suspend-user': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
- text: string;
};
};
};
@@ -10337,31 +10671,21 @@ export type operations = {
};
};
/**
- * admin/roles/create
+ * admin/system-webhook/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- admin___roles___create: {
+ 'admin___system-webhook___create': {
requestBody: {
content: {
'application/json': {
+ isActive: boolean;
name: string;
- description: string;
- color: string | null;
- iconUrl: string | null;
- /** @enum {string} */
- target: 'manual' | 'conditional';
- condFormula: Record<string, never>;
- isPublic: boolean;
- isModerator: boolean;
- isAdministrator: boolean;
- /** @default false */
- isExplorable?: boolean;
- asBadge: boolean;
- canEditMembersByModerator: boolean;
- displayOrder: number;
- policies: Record<string, never>;
+ on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
+ url: string;
+ secret: string;
};
};
};
@@ -10369,7 +10693,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Role'];
+ 'application/json': components['schemas']['SystemWebhook'];
};
};
/** @description Client error */
@@ -10405,17 +10729,18 @@ export type operations = {
};
};
/**
- * admin/roles/delete
+ * admin/system-webhook/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- admin___roles___delete: {
+ 'admin___system-webhook___delete': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- roleId: string;
+ id: string;
};
};
};
@@ -10457,17 +10782,26 @@ export type operations = {
};
};
/**
- * admin/roles/list
+ * admin/system-webhook/list
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- admin___roles___list: {
+ 'admin___system-webhook___list': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ isActive?: boolean;
+ on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Role'][];
+ 'application/json': components['schemas']['SystemWebhook'][];
};
};
/** @description Client error */
@@ -10503,17 +10837,18 @@ export type operations = {
};
};
/**
- * admin/roles/show
+ * admin/system-webhook/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- admin___roles___show: {
+ 'admin___system-webhook___show': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- roleId: string;
+ id: string;
};
};
};
@@ -10521,7 +10856,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Role'];
+ 'application/json': components['schemas']['SystemWebhook'];
};
};
/** @description Client error */
@@ -10557,32 +10892,24 @@ export type operations = {
};
};
/**
- * admin/roles/update
+ * admin/system-webhook/test
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
*/
- admin___roles___update: {
+ 'admin___system-webhook___test': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- roleId: string;
- name?: string;
- description?: string;
- color?: string | null;
- iconUrl?: string | null;
+ webhookId: string;
/** @enum {string} */
- target?: 'manual' | 'conditional';
- condFormula?: Record<string, never>;
- isPublic?: boolean;
- isModerator?: boolean;
- isAdministrator?: boolean;
- isExplorable?: boolean;
- asBadge?: boolean;
- canEditMembersByModerator?: boolean;
- displayOrder?: number;
- policies?: Record<string, never>;
+ type: 'abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged';
+ override?: {
+ url?: string;
+ secret?: string;
+ };
};
};
};
@@ -10615,6 +10942,12 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
/** @description Internal server error */
500: {
content: {
@@ -10624,27 +10957,32 @@ export type operations = {
};
};
/**
- * admin/roles/assign
+ * admin/system-webhook/update
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
*/
- admin___roles___assign: {
+ 'admin___system-webhook___update': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- roleId: string;
- /** Format: misskey:id */
- userId: string;
- expiresAt?: number | null;
+ id: string;
+ isActive: boolean;
+ name: string;
+ on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
+ url: string;
+ secret: string;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['SystemWebhook'];
+ };
};
/** @description Client error */
400: {
@@ -10679,18 +11017,16 @@ export type operations = {
};
};
/**
- * admin/roles/unassign
+ * admin/unnsfw-user
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unnsfw-user*
*/
- admin___roles___unassign: {
+ 'admin___unnsfw-user': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- roleId: string;
- /** Format: misskey:id */
userId: string;
};
};
@@ -10733,16 +11069,17 @@ export type operations = {
};
};
/**
- * admin/roles/update-default-policies
+ * admin/unset-user-avatar
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar*
*/
- 'admin___roles___update-default-policies': {
+ 'admin___unset-user-avatar': {
requestBody: {
content: {
'application/json': {
- policies: Record<string, never>;
+ /** Format: misskey:id */
+ userId: string;
};
};
};
@@ -10784,98 +11121,24 @@ export type operations = {
};
};
/**
- * admin/roles/users
+ * admin/unset-user-banner
* @description No description provided.
*
- * **Credential required**: *No* / **Permission**: *read:admin:roles*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner*
*/
- admin___roles___users: {
+ 'admin___unset-user-banner': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- roleId: string;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- /** @default 10 */
- limit?: number;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': ({
- /** Format: misskey:id */
- id: string;
- /** Format: date-time */
- createdAt: string;
- user: components['schemas']['UserDetailed'];
- /** Format: date-time */
- expiresAt: string | null;
- })[];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * admin/system-webhook/create
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
- */
- 'admin___system-webhook___create': {
- requestBody: {
- content: {
- 'application/json': {
- isActive: boolean;
- name: string;
- on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
- url: string;
- secret: string;
+ userId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['SystemWebhook'];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -10910,18 +11173,17 @@ export type operations = {
};
};
/**
- * admin/system-webhook/delete
+ * admin/unsilence-user
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unsilence-user*
*/
- 'admin___system-webhook___delete': {
+ 'admin___unsilence-user': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- id: string;
+ userId: string;
};
};
};
@@ -10963,27 +11225,24 @@ export type operations = {
};
};
/**
- * admin/system-webhook/list
+ * admin/unsuspend-user
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user*
*/
- 'admin___system-webhook___list': {
+ 'admin___unsuspend-user': {
requestBody: {
content: {
'application/json': {
- isActive?: boolean;
- on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
+ /** Format: misskey:id */
+ userId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['SystemWebhook'][];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -11018,27 +11277,25 @@ export type operations = {
};
};
/**
- * admin/system-webhook/show
+ * admin/update-abuse-user-report
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
- 'admin___system-webhook___show': {
+ 'admin___update-abuse-user-report': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- id: string;
+ reportId: string;
+ moderationNote?: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['SystemWebhook'];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -11073,32 +11330,155 @@ export type operations = {
};
};
/**
- * admin/system-webhook/update
+ * admin/update-meta
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:meta*
*/
- 'admin___system-webhook___update': {
+ 'admin___update-meta': {
requestBody: {
content: {
'application/json': {
+ disableRegistration?: boolean | null;
+ pinnedUsers?: string[] | null;
+ hiddenTags?: string[] | null;
+ blockedHosts?: string[] | null;
+ sensitiveWords?: string[] | null;
+ prohibitedWords?: string[] | null;
+ prohibitedWordsForNameOfUser?: string[] | null;
+ themeColor?: string | null;
+ mascotImageUrl?: string | null;
+ bannerUrl?: string | null;
+ serverErrorImageUrl?: string | null;
+ infoImageUrl?: string | null;
+ notFoundImageUrl?: string | null;
+ iconUrl?: string | null;
+ app192IconUrl?: string | null;
+ app512IconUrl?: string | null;
+ sidebarLogoUrl?: string | null;
+ backgroundImageUrl?: string | null;
+ logoImageUrl?: string | null;
+ name?: string | null;
+ shortName?: string | null;
+ description?: string | null;
+ defaultLightTheme?: string | null;
+ defaultDarkTheme?: string | null;
+ defaultLike?: string | null;
+ cacheRemoteFiles?: boolean;
+ cacheRemoteSensitiveFiles?: boolean;
+ emailRequiredForSignup?: boolean;
+ approvalRequiredForSignup?: boolean;
+ enableHcaptcha?: boolean;
+ hcaptchaSiteKey?: string | null;
+ hcaptchaSecretKey?: string | null;
+ enableMcaptcha?: boolean;
+ mcaptchaSiteKey?: string | null;
+ mcaptchaInstanceUrl?: string | null;
+ mcaptchaSecretKey?: string | null;
+ enableRecaptcha?: boolean;
+ recaptchaSiteKey?: string | null;
+ recaptchaSecretKey?: string | null;
+ enableTurnstile?: boolean;
+ turnstileSiteKey?: string | null;
+ turnstileSecretKey?: string | null;
+ enableFC?: boolean;
+ fcSiteKey?: string | null;
+ fcSecretKey?: string | null;
+ enableTestcaptcha?: boolean;
+ /** @enum {string} */
+ sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
+ /** @enum {string} */
+ sensitiveMediaDetectionSensitivity?: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh';
+ setSensitiveFlagAutomatically?: boolean;
+ enableSensitiveMediaDetectionForVideos?: boolean;
+ enableBotTrending?: boolean;
/** Format: misskey:id */
- id: string;
- isActive: boolean;
- name: string;
- on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
- url: string;
- secret: string;
+ proxyAccountId?: string | null;
+ maintainerName?: string | null;
+ maintainerEmail?: string | null;
+ langs?: string[];
+ deeplAuthKey?: string | null;
+ deeplIsPro?: boolean;
+ deeplFreeMode?: boolean;
+ deeplFreeInstance?: string | null;
+ enableEmail?: boolean;
+ email?: string | null;
+ smtpSecure?: boolean;
+ smtpHost?: string | null;
+ smtpPort?: number | null;
+ smtpUser?: string | null;
+ smtpPass?: string | null;
+ enableServiceWorker?: boolean;
+ swPublicKey?: string | null;
+ swPrivateKey?: string | null;
+ tosUrl?: string | null;
+ repositoryUrl?: string | null;
+ feedbackUrl?: string | null;
+ impressumUrl?: string | null;
+ donationUrl?: string | null;
+ privacyPolicyUrl?: string | null;
+ inquiryUrl?: string | null;
+ useObjectStorage?: boolean;
+ objectStorageBaseUrl?: string | null;
+ objectStorageBucket?: string | null;
+ objectStoragePrefix?: string | null;
+ objectStorageEndpoint?: string | null;
+ objectStorageRegion?: string | null;
+ objectStoragePort?: number | null;
+ objectStorageAccessKey?: string | null;
+ objectStorageSecretKey?: string | null;
+ objectStorageUseSSL?: boolean;
+ objectStorageUseProxy?: boolean;
+ objectStorageSetPublicRead?: boolean;
+ objectStorageS3ForcePathStyle?: boolean;
+ enableIpLogging?: boolean;
+ enableActiveEmailValidation?: boolean;
+ enableVerifymailApi?: boolean;
+ verifymailAuthKey?: string | null;
+ enableTruemailApi?: boolean;
+ truemailInstance?: string | null;
+ truemailAuthKey?: string | null;
+ enableChartsForRemoteUser?: boolean;
+ enableChartsForFederatedInstances?: boolean;
+ enableStatsForFederatedInstances?: boolean;
+ enableServerMachineStats?: boolean;
+ enableAchievements?: boolean;
+ robotsTxt?: string | null;
+ enableIdenticonGeneration?: boolean;
+ serverRules?: string[];
+ bannedEmailDomains?: string[];
+ preservedUsernames?: string[];
+ bubbleInstances?: string[];
+ manifestJsonOverride?: string;
+ enableFanoutTimeline?: boolean;
+ enableFanoutTimelineDbFallback?: boolean;
+ perLocalUserUserTimelineCacheMax?: number;
+ perRemoteUserUserTimelineCacheMax?: number;
+ perUserHomeTimelineCacheMax?: number;
+ perUserListTimelineCacheMax?: number;
+ enableReactionsBuffering?: boolean;
+ notesPerOneAd?: number;
+ silencedHosts?: string[] | null;
+ mediaSilencedHosts?: string[] | null;
+ /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */
+ summalyProxy?: string | null;
+ urlPreviewEnabled?: boolean;
+ urlPreviewTimeout?: number;
+ urlPreviewMaximumContentLength?: number;
+ urlPreviewRequireContentLength?: boolean;
+ urlPreviewUserAgent?: string | null;
+ urlPreviewSummaryProxyUrl?: string | null;
+ trustedLinkUrlPatterns?: string[] | null;
+ /** @enum {string} */
+ federation?: 'all' | 'none' | 'specified';
+ federationHosts?: string[];
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['SystemWebhook'];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -11133,24 +11513,18 @@ export type operations = {
};
};
/**
- * admin/system-webhook/test
+ * admin/update-user-note
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:user-note*
*/
- 'admin___system-webhook___test': {
+ 'admin___update-user-note': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- webhookId: string;
- /** @enum {string} */
- type: 'abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged';
- override?: {
- url?: string;
- secret?: string;
- };
+ userId: string;
+ text: string;
};
};
};
@@ -11183,12 +11557,6 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
/** @description Internal server error */
500: {
content: {
@@ -12386,22 +12754,16 @@ export type operations = {
};
};
/**
- * channels/create
+ * bubble-game/ranking
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- channels___create: {
+ 'bubble-game___ranking': {
requestBody: {
content: {
'application/json': {
- name: string;
- description?: string | null;
- /** Format: misskey:id */
- bannerId?: string | null;
- color?: string;
- isSensitive?: boolean | null;
- allowRenoteToExternal?: boolean | null;
+ gameMode: string;
};
};
};
@@ -12409,7 +12771,12 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Channel'];
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ score: number;
+ user?: components['schemas']['UserLite'];
+ }[];
};
};
/** @description Client error */
@@ -12451,17 +12818,91 @@ export type operations = {
};
};
/**
- * channels/featured
+ * bubble-game/register
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- channels___featured: {
+ 'bubble-game___register': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ score: number;
+ seed: string;
+ logs: number[][];
+ gameMode: string;
+ gameVersion: number;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * channels/create
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
+ */
+ channels___create: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ name: string;
+ description?: string | null;
+ /** Format: misskey:id */
+ bannerId?: string | null;
+ color?: string;
+ isSensitive?: boolean | null;
+ allowRenoteToExternal?: boolean | null;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Channel'][];
+ 'application/json': components['schemas']['Channel'];
};
};
/** @description Client error */
@@ -12503,12 +12944,12 @@ export type operations = {
};
};
/**
- * channels/follow
+ * channels/favorite
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- channels___follow: {
+ channels___favorite: {
requestBody: {
content: {
'application/json': {
@@ -12561,30 +13002,76 @@ export type operations = {
};
};
/**
- * channels/followed
+ * channels/featured
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:channels*
+ * **Credential required**: *No*
*/
- channels___followed: {
+ channels___featured: {
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Channel'][];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * channels/follow
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
+ */
+ channels___follow: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- /** @default 5 */
- limit?: number;
+ channelId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Channel'][];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -12625,12 +13112,12 @@ export type operations = {
};
};
/**
- * channels/owned
+ * channels/followed
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:channels*
*/
- channels___owned: {
+ channels___followed: {
requestBody: {
content: {
'application/json': {
@@ -12689,25 +13176,17 @@ export type operations = {
};
};
/**
- * channels/show
+ * channels/my-favorites
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:channels*
*/
- channels___show: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- channelId: string;
- };
- };
- };
+ 'channels___my-favorites': {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Channel'];
+ 'application/json': components['schemas']['Channel'][];
};
};
/** @description Client error */
@@ -12749,34 +13228,21 @@ export type operations = {
};
};
/**
- * channels/timeline
+ * channels/owned
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:channels*
*/
- channels___timeline: {
+ channels___owned: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- channelId: string;
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
- sinceDate?: number;
- untilDate?: number;
- /** @default false */
- allowPartial?: boolean;
- /** @default true */
- withRenotes?: boolean;
- /**
- * @description Only show notes that have attached files.
- * @default false
- */
- withFiles?: boolean;
+ /** @default 5 */
+ limit?: number;
};
};
};
@@ -12784,7 +13250,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Note'][];
+ 'application/json': components['schemas']['Channel'][];
};
};
/** @description Client error */
@@ -12826,24 +13292,36 @@ export type operations = {
};
};
/**
- * channels/unfollow
+ * channels/search
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- channels___unfollow: {
+ channels___search: {
requestBody: {
content: {
'application/json': {
+ query: string;
+ /**
+ * @default nameAndDescription
+ * @enum {string}
+ */
+ type?: 'nameAndDescription' | 'nameOnly';
/** Format: misskey:id */
- channelId: string;
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ /** @default 5 */
+ limit?: number;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Channel'][];
+ };
};
/** @description Client error */
400: {
@@ -12884,26 +13362,17 @@ export type operations = {
};
};
/**
- * channels/update
+ * channels/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- channels___update: {
+ channels___show: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
channelId: string;
- name?: string;
- description?: string | null;
- /** Format: misskey:id */
- bannerId?: string | null;
- isArchived?: boolean | null;
- pinnedNoteIds?: string[];
- color?: string;
- isSensitive?: boolean | null;
- allowRenoteToExternal?: boolean | null;
};
};
};
@@ -12953,24 +13422,43 @@ export type operations = {
};
};
/**
- * channels/favorite
+ * channels/timeline
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:channels*
+ * **Credential required**: *No*
*/
- channels___favorite: {
+ channels___timeline: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
channelId: string;
+ /** @default 10 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ sinceDate?: number;
+ untilDate?: number;
+ /** @default false */
+ allowPartial?: boolean;
+ /** @default true */
+ withRenotes?: boolean;
+ /**
+ * @description Only show notes that have attached files.
+ * @default false
+ */
+ withFiles?: boolean;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Note'][];
+ };
};
/** @description Client error */
400: {
@@ -13069,19 +13557,25 @@ export type operations = {
};
};
/**
- * channels/my-favorites
+ * channels/unfollow
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:channels*
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- 'channels___my-favorites': {
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Channel'][];
+ channels___unfollow: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ channelId: string;
};
};
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
/** @description Client error */
400: {
content: {
@@ -13121,27 +13615,26 @@ export type operations = {
};
};
/**
- * channels/search
+ * channels/update
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:channels*
*/
- channels___search: {
+ channels___update: {
requestBody: {
content: {
'application/json': {
- query: string;
- /**
- * @default nameAndDescription
- * @enum {string}
- */
- type?: 'nameAndDescription' | 'nameOnly';
/** Format: misskey:id */
- sinceId?: string;
+ channelId: string;
+ name?: string;
+ description?: string | null;
/** Format: misskey:id */
- untilId?: string;
- /** @default 5 */
- limit?: number;
+ bannerId?: string | null;
+ isArchived?: boolean | null;
+ pinnedNoteIds?: string[];
+ color?: string;
+ isSensitive?: boolean | null;
+ allowRenoteToExternal?: boolean | null;
};
};
};
@@ -13149,7 +13642,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Channel'][];
+ 'application/json': components['schemas']['Channel'];
};
};
/** @description Client error */
@@ -14197,26 +14690,28 @@ export type operations = {
};
};
/**
- * clips/remove-note
+ * clips/create
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- 'clips___remove-note': {
+ clips___create: {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- clipId: string;
- /** Format: misskey:id */
- noteId: string;
+ name: string;
+ /** @default false */
+ isPublic?: boolean;
+ description?: string | null;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Clip'];
+ };
};
/** @description Client error */
400: {
@@ -14257,28 +14752,24 @@ export type operations = {
};
};
/**
- * clips/create
+ * clips/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- clips___create: {
+ clips___delete: {
requestBody: {
content: {
'application/json': {
- name: string;
- /** @default false */
- isPublic?: boolean;
- description?: string | null;
+ /** Format: misskey:id */
+ clipId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Clip'];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -14319,12 +14810,12 @@ export type operations = {
};
};
/**
- * clips/delete
+ * clips/favorite
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *Yes* / **Permission**: *write:clip-favorite*
*/
- clips___delete: {
+ clips___favorite: {
requestBody: {
content: {
'application/json': {
@@ -14429,31 +14920,17 @@ export type operations = {
};
};
/**
- * clips/notes
+ * clips/my-favorites
* @description No description provided.
*
- * **Credential required**: *No* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *read:clip-favorite*
*/
- clips___notes: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- clipId: string;
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- };
- };
- };
+ 'clips___my-favorites': {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Note'][];
+ 'application/json': components['schemas']['Clip'][];
};
};
/** @description Client error */
@@ -14495,17 +14972,23 @@ export type operations = {
};
};
/**
- * clips/show
+ * clips/notes
* @description No description provided.
*
* **Credential required**: *No* / **Permission**: *read:account*
*/
- clips___show: {
+ clips___notes: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
clipId: string;
+ /** @default 10 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
};
};
};
@@ -14513,7 +14996,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Clip'];
+ 'application/json': components['schemas']['Note'][];
};
};
/** @description Client error */
@@ -14555,29 +15038,26 @@ export type operations = {
};
};
/**
- * clips/update
+ * clips/remove-note
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- clips___update: {
+ 'clips___remove-note': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
clipId: string;
- name?: string;
- isPublic?: boolean;
- description?: string | null;
+ /** Format: misskey:id */
+ noteId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Clip'];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -14618,12 +15098,12 @@ export type operations = {
};
};
/**
- * clips/favorite
+ * clips/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:clip-favorite*
+ * **Credential required**: *No* / **Permission**: *read:account*
*/
- clips___favorite: {
+ clips___show: {
requestBody: {
content: {
'application/json': {
@@ -14633,9 +15113,11 @@ export type operations = {
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Clip'];
+ };
};
/** @description Client error */
400: {
@@ -14734,17 +15216,28 @@ export type operations = {
};
};
/**
- * clips/my-favorites
+ * clips/update
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:clip-favorite*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- 'clips___my-favorites': {
+ clips___update: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ clipId: string;
+ name?: string;
+ isPublic?: boolean;
+ description?: string | null;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Clip'][];
+ 'application/json': components['schemas']['Clip'];
};
};
/** @description Client error */
@@ -15174,16 +15667,21 @@ export type operations = {
};
};
/**
- * drive/files/find-by-hash
- * @description Search for a drive file by a hash of the contents.
+ * drive/files/find
+ * @description Search for a drive file by the given parameters.
*
* **Credential required**: *Yes* / **Permission**: *read:drive*
*/
- 'drive___files___find-by-hash': {
+ drive___files___find: {
requestBody: {
content: {
'application/json': {
- md5: string;
+ name: string;
+ /**
+ * Format: misskey:id
+ * @default null
+ */
+ folderId?: string | null;
};
};
};
@@ -15233,21 +15731,16 @@ export type operations = {
};
};
/**
- * drive/files/find
- * @description Search for a drive file by the given parameters.
+ * drive/files/find-by-hash
+ * @description Search for a drive file by a hash of the contents.
*
* **Credential required**: *Yes* / **Permission**: *read:drive*
*/
- drive___files___find: {
+ 'drive___files___find-by-hash': {
requestBody: {
content: {
'application/json': {
- name: string;
- /**
- * Format: misskey:id
- * @default null
- */
- folderId?: string | null;
+ md5: string;
};
};
};
@@ -15998,6 +16491,119 @@ export type operations = {
};
};
/**
+ * emoji
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ emoji: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ name: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['EmojiDetailed'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * emojis
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ emojis: {
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ emojis: components['schemas']['EmojiSimple'][];
+ };
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* endpoint
* @description No description provided.
*
@@ -16445,6 +17051,71 @@ export type operations = {
};
};
/**
+ * federation/stats
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ federation___stats: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 10 */
+ limit?: number;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ topSubInstances: components['schemas']['FederationInstance'][];
+ otherFollowersCount: number;
+ topPubInstances: components['schemas']['FederationInstance'][];
+ otherFollowingCount: number;
+ };
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* federation/update-remote-user
* @description No description provided.
*
@@ -16568,17 +17239,80 @@ export type operations = {
};
};
/**
- * federation/stats
+ * fetch-external-resources
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
+ */
+ 'fetch-external-resources': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ url: string;
+ hash: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ type: string;
+ data: string;
+ };
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * fetch-rss
* @description No description provided.
*
* **Credential required**: *No*
*/
- federation___stats: {
+ 'fetch-rss': {
requestBody: {
content: {
'application/json': {
- /** @default 10 */
- limit?: number;
+ url: string;
};
};
};
@@ -16587,10 +17321,52 @@ export type operations = {
200: {
content: {
'application/json': {
- topSubInstances: components['schemas']['FederationInstance'][];
- otherFollowersCount: number;
- topPubInstances: components['schemas']['FederationInstance'][];
- otherFollowingCount: number;
+ image?: {
+ link?: string;
+ url: string;
+ title?: string;
+ };
+ paginationLinks?: {
+ self?: string;
+ first?: string;
+ next?: string;
+ last?: string;
+ prev?: string;
+ };
+ link?: string;
+ title?: string;
+ items: {
+ link?: string;
+ guid?: string;
+ title?: string;
+ pubDate?: string;
+ creator?: string;
+ summary?: string;
+ content?: string;
+ isoDate?: string;
+ categories?: string[];
+ contentSnippet?: string;
+ enclosure?: {
+ url: string;
+ length?: number;
+ type?: string;
+ };
+ }[];
+ feedUrl?: string;
+ description?: string;
+ itunes?: {
+ image?: string;
+ owner?: {
+ name?: string;
+ email?: string;
+ };
+ author?: string;
+ summary?: string;
+ explicit?: string;
+ categories?: string[];
+ keywords?: string[];
+ [key: string]: unknown;
+ };
};
};
};
@@ -16633,18 +17409,144 @@ export type operations = {
};
};
/**
- * following/create
+ * flash/create
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:following*
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
*/
- following___create: {
+ flash___create: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ title: string;
+ summary: string;
+ script: string;
+ permissions: string[];
+ /**
+ * @default public
+ * @enum {string}
+ */
+ visibility?: 'public' | 'private';
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Flash'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * flash/delete
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
+ */
+ flash___delete: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- userId: string;
- withReplies?: boolean;
+ flashId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * flash/featured
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ flash___featured: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 0 */
+ offset?: number;
+ /** @default 10 */
+ limit?: number;
};
};
};
@@ -16652,7 +17554,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['UserLite'];
+ 'application/json': components['schemas']['Flash'][];
};
};
/** @description Client error */
@@ -16694,17 +17596,79 @@ export type operations = {
};
};
/**
- * following/delete
+ * flash/like
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:following*
+ * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
*/
- following___delete: {
+ flash___like: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- userId: string;
+ flashId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * flash/my
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:flash*
+ */
+ flash___my: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 10 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
};
};
};
@@ -16712,7 +17676,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['UserLite'];
+ 'application/json': components['schemas']['Flash'][];
};
};
/** @description Client error */
@@ -16754,19 +17718,267 @@ export type operations = {
};
};
/**
- * following/update
+ * flash/my-likes
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:flash-likes*
+ */
+ 'flash___my-likes': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 10 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ /** Format: id */
+ id: string;
+ flash: components['schemas']['Flash'];
+ }[];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * flash/show
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ flash___show: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ flashId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Flash'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * flash/unlike
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ */
+ flash___unlike: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ flashId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * flash/update
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:flash*
+ */
+ flash___update: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ flashId: string;
+ title?: string;
+ summary?: string;
+ script?: string;
+ permissions?: string[];
+ /** @enum {string} */
+ visibility?: 'public' | 'private';
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * following/create
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- following___update: {
+ following___create: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
- /** @enum {string} */
- notify?: 'normal' | 'none';
withReplies?: boolean;
};
};
@@ -16817,25 +18029,26 @@ export type operations = {
};
};
/**
- * following/update-all
+ * following/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- 'following___update-all': {
+ following___delete: {
requestBody: {
content: {
'application/json': {
- /** @enum {string} */
- notify?: 'normal' | 'none';
- withReplies?: boolean;
+ /** Format: misskey:id */
+ userId: string;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['UserLite'];
+ };
};
/** @description Client error */
400: {
@@ -17123,6 +18336,64 @@ export type operations = {
};
};
/**
+ * following/requests/reject
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:following*
+ */
+ following___requests___reject: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ userId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* following/requests/sent
* @description No description provided.
*
@@ -17192,17 +18463,81 @@ export type operations = {
};
};
/**
- * following/requests/reject
+ * following/update
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:following*
*/
- following___requests___reject: {
+ following___update: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
+ /** @enum {string} */
+ notify?: 'normal' | 'none';
+ withReplies?: boolean;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['UserLite'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * following/update-all
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:following*
+ */
+ 'following___update-all': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @enum {string} */
+ notify?: 'normal' | 'none';
+ withReplies?: boolean;
};
};
};
@@ -17790,19 +19125,27 @@ export type operations = {
};
};
/**
- * get-online-users-count
+ * get-avatar-decorations
* @description No description provided.
*
* **Credential required**: *No*
*/
- 'get-online-users-count': {
+ 'get-avatar-decorations': {
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': {
- count: number;
- };
+ /**
+ * Format: id
+ * @example xxxxxxxxxx
+ */
+ id: string;
+ name: string;
+ description: string;
+ url: string;
+ roleIdsThatCanBeUsedThisDecoration: string[];
+ }[];
};
};
/** @description Client error */
@@ -17844,27 +19187,19 @@ export type operations = {
};
};
/**
- * get-avatar-decorations
+ * get-online-users-count
* @description No description provided.
*
* **Credential required**: *No*
*/
- 'get-avatar-decorations': {
+ 'get-online-users-count': {
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': {
- /**
- * Format: id
- * @example xxxxxxxxxx
- */
- id: string;
- name: string;
- description: string;
- url: string;
- roleIdsThatCanBeUsedThisDecoration: string[];
- }[];
+ count: number;
+ };
};
};
/** @description Client error */
@@ -18463,13 +19798,13 @@ export type operations = {
};
};
/**
- * i/2fa/register-key
+ * i/2fa/register
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___2fa___register-key': {
+ i___2fa___register: {
requestBody: {
content: {
'application/json': {
@@ -18483,39 +19818,11 @@ export type operations = {
200: {
content: {
'application/json': {
- rp: {
- id?: string;
- };
- user: {
- id: string;
- name: string;
- displayName: string;
- };
- challenge: string;
- pubKeyCredParams: {
- type: string;
- alg: number;
- }[];
- timeout: number | null;
- excludeCredentials: (({
- id: string;
- type: string;
- transports: ('ble' | 'cable' | 'hybrid' | 'internal' | 'nfc' | 'smart-card' | 'usb')[];
- })[]) | null;
- authenticatorSelection: ({
- /** @enum {string} */
- authenticatorAttachment: 'cross-platform' | 'platform';
- requireResidentKey: boolean;
- /** @enum {string} */
- userVerification: 'discouraged' | 'preferred' | 'required';
- }) | null;
- /** @enum {string|null} */
- attestation: 'direct' | 'enterprise' | 'indirect' | 'none' | null;
- extensions: ({
- appid: string | null;
- credProps: boolean | null;
- hmacCreateSecret: boolean | null;
- }) | null;
+ qr: string;
+ url: string;
+ secret: string;
+ label: string;
+ issuer: string;
};
};
};
@@ -18558,13 +19865,13 @@ export type operations = {
};
};
/**
- * i/2fa/register
+ * i/2fa/register-key
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- i___2fa___register: {
+ 'i___2fa___register-key': {
requestBody: {
content: {
'application/json': {
@@ -18578,11 +19885,39 @@ export type operations = {
200: {
content: {
'application/json': {
- qr: string;
- url: string;
- secret: string;
- label: string;
- issuer: string;
+ rp: {
+ id?: string;
+ };
+ user: {
+ id: string;
+ name: string;
+ displayName: string;
+ };
+ challenge: string;
+ pubKeyCredParams: {
+ type: string;
+ alg: number;
+ }[];
+ timeout: number | null;
+ excludeCredentials: (({
+ id: string;
+ type: string;
+ transports: ('ble' | 'cable' | 'hybrid' | 'internal' | 'nfc' | 'smart-card' | 'usb')[];
+ })[]) | null;
+ authenticatorSelection: ({
+ /** @enum {string} */
+ authenticatorAttachment: 'cross-platform' | 'platform';
+ requireResidentKey: boolean;
+ /** @enum {string} */
+ userVerification: 'discouraged' | 'preferred' | 'required';
+ }) | null;
+ /** @enum {string|null} */
+ attestation: 'direct' | 'enterprise' | 'indirect' | 'none' | null;
+ extensions: ({
+ appid: string | null;
+ credProps: boolean | null;
+ hmacCreateSecret: boolean | null;
+ }) | null;
};
};
};
@@ -18625,17 +19960,18 @@ export type operations = {
};
};
/**
- * i/2fa/update-key
+ * i/2fa/remove-key
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___2fa___update-key': {
+ 'i___2fa___remove-key': {
requestBody: {
content: {
'application/json': {
- name: string;
+ password: string;
+ token?: string | null;
credentialId: string;
};
};
@@ -18684,19 +20020,18 @@ export type operations = {
};
};
/**
- * i/2fa/remove-key
+ * i/2fa/unregister
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___2fa___remove-key': {
+ i___2fa___unregister: {
requestBody: {
content: {
'application/json': {
password: string;
token?: string | null;
- credentialId: string;
};
};
};
@@ -18744,18 +20079,18 @@ export type operations = {
};
};
/**
- * i/2fa/unregister
+ * i/2fa/update-key
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- i___2fa___unregister: {
+ 'i___2fa___update-key': {
requestBody: {
content: {
'application/json': {
- password: string;
- token?: string | null;
+ name: string;
+ credentialId: string;
};
};
};
@@ -18948,17 +20283,19 @@ export type operations = {
};
};
/**
- * i/claim-achievement
+ * i/change-password
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
*/
- 'i___claim-achievement': {
+ 'i___change-password': {
requestBody: {
content: {
'application/json': {
- /** @enum {string} */
- name: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead';
+ currentPassword: string;
+ newPassword: string;
+ token?: string | null;
};
};
};
@@ -19006,19 +20343,17 @@ export type operations = {
};
};
/**
- * i/change-password
+ * i/claim-achievement
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- 'i___change-password': {
+ 'i___claim-achievement': {
requestBody: {
content: {
'application/json': {
- currentPassword: string;
- newPassword: string;
- token?: string | null;
+ /** @enum {string} */
+ name: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead';
};
};
};
@@ -19125,13 +20460,13 @@ export type operations = {
};
};
/**
- * i/export-data
+ * i/export-antennas
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___export-data': {
+ 'i___export-antennas': {
responses: {
/** @description OK (without any results) */
204: {
@@ -19227,23 +20562,13 @@ export type operations = {
};
};
/**
- * i/export-following
+ * i/export-clips
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___export-following': {
- requestBody: {
- content: {
- 'application/json': {
- /** @default false */
- excludeMuting?: boolean;
- /** @default false */
- excludeInactive?: boolean;
- };
- };
- };
+ 'i___export-clips': {
responses: {
/** @description OK (without any results) */
204: {
@@ -19288,13 +20613,13 @@ export type operations = {
};
};
/**
- * i/export-mute
+ * i/export-data
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___export-mute': {
+ 'i___export-data': {
responses: {
/** @description OK (without any results) */
204: {
@@ -19339,13 +20664,13 @@ export type operations = {
};
};
/**
- * i/export-notes
+ * i/export-favorites
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___export-notes': {
+ 'i___export-favorites': {
responses: {
/** @description OK (without any results) */
204: {
@@ -19390,13 +20715,23 @@ export type operations = {
};
};
/**
- * i/export-clips
+ * i/export-following
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___export-clips': {
+ 'i___export-following': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default false */
+ excludeMuting?: boolean;
+ /** @default false */
+ excludeInactive?: boolean;
+ };
+ };
+ };
responses: {
/** @description OK (without any results) */
204: {
@@ -19441,13 +20776,13 @@ export type operations = {
};
};
/**
- * i/export-favorites
+ * i/export-mute
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___export-favorites': {
+ 'i___export-mute': {
responses: {
/** @description OK (without any results) */
204: {
@@ -19492,13 +20827,13 @@ export type operations = {
};
};
/**
- * i/export-user-lists
+ * i/export-notes
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___export-user-lists': {
+ 'i___export-notes': {
responses: {
/** @description OK (without any results) */
204: {
@@ -19543,13 +20878,13 @@ export type operations = {
};
};
/**
- * i/export-antennas
+ * i/export-user-lists
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___export-antennas': {
+ 'i___export-user-lists': {
responses: {
/** @description OK (without any results) */
204: {
@@ -19790,13 +21125,13 @@ export type operations = {
};
};
/**
- * i/import-blocking
+ * i/import-antennas
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___import-blocking': {
+ 'i___import-antennas': {
requestBody: {
content: {
'application/json': {
@@ -19849,19 +21184,18 @@ export type operations = {
};
};
/**
- * i/import-following
+ * i/import-blocking
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___import-following': {
+ 'i___import-blocking': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
fileId: string;
- withReplies?: boolean;
};
};
};
@@ -19909,19 +21243,19 @@ export type operations = {
};
};
/**
- * i/import-notes
+ * i/import-following
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___import-notes': {
+ 'i___import-following': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
fileId: string;
- type?: string | null;
+ withReplies?: boolean;
};
};
};
@@ -20028,18 +21362,19 @@ export type operations = {
};
};
/**
- * i/import-user-lists
+ * i/import-notes
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___import-user-lists': {
+ 'i___import-notes': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
fileId: string;
+ type?: string | null;
};
};
};
@@ -20087,13 +21422,13 @@ export type operations = {
};
};
/**
- * i/import-antennas
+ * i/import-user-lists
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'i___import-antennas': {
+ 'i___import-user-lists': {
requestBody: {
content: {
'application/json': {
@@ -20146,6 +21481,66 @@ export type operations = {
};
};
/**
+ * i/move
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
+ */
+ i___move: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ moveToAccount: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': Record<string, never>;
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* i/notifications
* @description No description provided.
*
@@ -20640,15 +22035,16 @@ export type operations = {
};
};
/**
- * i/registry/get-all
+ * i/registry/get
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- 'i___registry___get-all': {
+ i___registry___get: {
requestBody: {
content: {
'application/json': {
+ key: string;
/** @default [] */
scope: string[];
domain?: string | null;
@@ -20701,25 +22097,27 @@ export type operations = {
};
};
/**
- * i/registry/get-unsecure
+ * i/registry/get-all
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- 'i___registry___get-unsecure': {
+ 'i___registry___get-all': {
requestBody: {
content: {
'application/json': {
- key: string;
/** @default [] */
- scope?: string[];
+ scope: string[];
+ domain?: string | null;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': Record<string, never>;
+ };
};
/** @description Client error */
400: {
@@ -20825,28 +22223,25 @@ export type operations = {
};
};
/**
- * i/registry/get
+ * i/registry/get-unsecure
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- i___registry___get: {
+ 'i___registry___get-unsecure': {
requestBody: {
content: {
'application/json': {
key: string;
/** @default [] */
- scope: string[];
- domain?: string | null;
+ scope?: string[];
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': Record<string, never>;
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -20887,12 +22282,12 @@ export type operations = {
};
};
/**
- * i/registry/keys-with-type
+ * i/registry/keys
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- 'i___registry___keys-with-type': {
+ i___registry___keys: {
requestBody: {
content: {
'application/json': {
@@ -20906,9 +22301,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- [key: string]: string;
- };
+ 'application/json': string[];
};
};
/** @description Client error */
@@ -20950,12 +22343,12 @@ export type operations = {
};
};
/**
- * i/registry/keys
+ * i/registry/keys-with-type
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- i___registry___keys: {
+ 'i___registry___keys-with-type': {
requestBody: {
content: {
'application/json': {
@@ -20969,7 +22362,9 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': string[];
+ 'application/json': {
+ [key: string]: string;
+ };
};
};
/** @description Client error */
@@ -21373,68 +22768,6 @@ export type operations = {
};
};
/**
- * i/update-email
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
- */
- 'i___update-email': {
- requestBody: {
- content: {
- 'application/json': {
- password: string;
- email?: string | null;
- token?: string | null;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['MeDetailed'];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
* i/update
* @description No description provided.
*
@@ -21631,6 +22964,9 @@ export type operations = {
};
emailNotificationTypes?: string[];
alsoKnownAs?: string[];
+ defaultCW?: string | null;
+ /** @enum {string} */
+ defaultCWPriority?: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
};
};
};
@@ -21680,17 +23016,19 @@ export type operations = {
};
};
/**
- * i/move
+ * i/update-email
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- i___move: {
+ 'i___update-email': {
requestBody: {
content: {
'application/json': {
- moveToAccount: string;
+ password: string;
+ email?: string | null;
+ token?: string | null;
};
};
};
@@ -21698,7 +23036,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': Record<string, never>;
+ 'application/json': components['schemas']['MeDetailed'];
};
};
/** @description Client error */
@@ -21816,32 +23154,25 @@ export type operations = {
};
};
/**
- * i/webhooks/list
+ * i/webhooks/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- i___webhooks___list: {
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': ({
- /** Format: misskey:id */
- id: string;
- /** Format: misskey:id */
- userId: string;
- name: string;
- on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
- url: string;
- secret: string;
- active: boolean;
- /** Format: date-time */
- latestSentAt: string | null;
- latestStatus: number | null;
- })[];
+ i___webhooks___delete: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ webhookId: string;
};
};
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
/** @description Client error */
400: {
content: {
@@ -21881,38 +23212,30 @@ export type operations = {
};
};
/**
- * i/webhooks/show
+ * i/webhooks/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- i___webhooks___show: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- webhookId: string;
- };
- };
- };
+ i___webhooks___list: {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- /** Format: misskey:id */
- id: string;
- /** Format: misskey:id */
- userId: string;
- name: string;
- on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
- url: string;
- secret: string;
- active: boolean;
- /** Format: date-time */
- latestSentAt: string | null;
- latestStatus: number | null;
- };
+ 'application/json': ({
+ /** Format: misskey:id */
+ id: string;
+ /** Format: misskey:id */
+ userId: string;
+ name: string;
+ on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
+ url: string;
+ secret: string;
+ active: boolean;
+ /** Format: date-time */
+ latestSentAt: string | null;
+ latestStatus: number | null;
+ })[];
};
};
/** @description Client error */
@@ -21954,29 +23277,39 @@ export type operations = {
};
};
/**
- * i/webhooks/update
+ * i/webhooks/show
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- i___webhooks___update: {
+ i___webhooks___show: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
webhookId: string;
- name?: string;
- url?: string;
- secret?: string | null;
- on?: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
- active?: boolean;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ /** Format: misskey:id */
+ userId: string;
+ name: string;
+ on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
+ url: string;
+ secret: string;
+ active: boolean;
+ /** Format: date-time */
+ latestSentAt: string | null;
+ latestStatus: number | null;
+ };
+ };
};
/** @description Client error */
400: {
@@ -22017,17 +23350,24 @@ export type operations = {
};
};
/**
- * i/webhooks/delete
+ * i/webhooks/test
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- i___webhooks___delete: {
+ i___webhooks___test: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
webhookId: string;
+ /** @enum {string} */
+ type: 'mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited';
+ override?: {
+ url?: string;
+ secret?: string;
+ };
};
};
};
@@ -22075,24 +23415,22 @@ export type operations = {
};
};
/**
- * i/webhooks/test
+ * i/webhooks/update
* @description No description provided.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- i___webhooks___test: {
+ i___webhooks___update: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
webhookId: string;
- /** @enum {string} */
- type: 'mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited';
- override?: {
- url?: string;
- secret?: string;
- };
+ name?: string;
+ url?: string;
+ secret?: string | null;
+ on?: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
+ active?: boolean;
};
};
};
@@ -22250,70 +23588,6 @@ export type operations = {
};
};
/**
- * invite/list
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *read:invite-codes*
- */
- invite___list: {
- requestBody: {
- content: {
- 'application/json': {
- /** @default 30 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['InviteCode'][];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
* invite/limit
* @description No description provided.
*
@@ -22368,79 +23642,29 @@ export type operations = {
};
};
/**
- * meta
+ * invite/list
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:invite-codes*
*/
- meta: {
+ invite___list: {
requestBody: {
content: {
'application/json': {
- /** @default true */
- detail?: boolean;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['MetaLite'] | components['schemas']['MetaDetailed'];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
+ /** @default 30 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
};
};
};
- };
- /**
- * emojis
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- emojis: {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- emojis: components['schemas']['EmojiSimple'][];
- };
+ 'application/json': components['schemas']['InviteCode'][];
};
};
/** @description Client error */
@@ -22482,16 +23706,17 @@ export type operations = {
};
};
/**
- * emoji
+ * meta
* @description No description provided.
*
* **Credential required**: *No*
*/
- emoji: {
+ meta: {
requestBody: {
content: {
'application/json': {
- name: string;
+ /** @default true */
+ detail?: boolean;
};
};
};
@@ -22499,7 +23724,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['EmojiDetailed'];
+ 'application/json': components['schemas']['MetaLite'] | components['schemas']['MetaDetailed'];
};
};
/** @description Client error */
@@ -22789,24 +24014,28 @@ export type operations = {
};
};
/**
- * renote-mute/create
+ * my/apps
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:mutes*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- 'renote-mute___create': {
+ my___apps: {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- userId: string;
+ /** @default 10 */
+ limit?: number;
+ /** @default 0 */
+ offset?: number;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['App'][];
+ };
};
/** @description Client error */
400: {
@@ -22847,24 +24076,36 @@ export type operations = {
};
};
/**
- * renote-mute/delete
+ * notes
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:mutes*
+ * **Credential required**: *No*
*/
- 'renote-mute___delete': {
+ notes: {
requestBody: {
content: {
'application/json': {
+ /** @default false */
+ local?: boolean;
+ reply?: boolean;
+ renote?: boolean;
+ withFiles?: boolean;
+ poll?: boolean;
+ /** @default 10 */
+ limit?: number;
/** Format: misskey:id */
- userId: string;
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Note'][];
+ };
};
/** @description Client error */
400: {
@@ -22905,21 +24146,29 @@ export type operations = {
};
};
/**
- * renote-mute/list
+ * notes/bubble-timeline
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:mutes*
+ * **Credential required**: *No*
*/
- 'renote-mute___list': {
+ 'notes___bubble-timeline': {
requestBody: {
content: {
'application/json': {
- /** @default 30 */
+ /** @default false */
+ withFiles?: boolean;
+ /** @default true */
+ withBots?: boolean;
+ /** @default true */
+ withRenotes?: boolean;
+ /** @default 10 */
limit?: number;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
+ sinceDate?: number;
+ untilDate?: number;
};
};
};
@@ -22927,7 +24176,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['RenoteMuting'][];
+ 'application/json': components['schemas']['Note'][];
};
};
/** @description Client error */
@@ -22969,19 +24218,25 @@ export type operations = {
};
};
/**
- * my/apps
+ * notes/children
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *No*
*/
- my___apps: {
+ notes___children: {
requestBody: {
content: {
'application/json': {
+ /** Format: misskey:id */
+ noteId: string;
/** @default 10 */
limit?: number;
- /** @default 0 */
- offset?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ /** @default true */
+ showQuotes?: boolean;
};
};
};
@@ -22989,7 +24244,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['App'][];
+ 'application/json': components['schemas']['Note'][];
};
};
/** @description Client error */
@@ -23031,27 +24286,17 @@ export type operations = {
};
};
/**
- * notes
+ * notes/clips
* @description No description provided.
*
* **Credential required**: *No*
*/
- notes: {
+ notes___clips: {
requestBody: {
content: {
'application/json': {
- /** @default false */
- local?: boolean;
- reply?: boolean;
- renote?: boolean;
- withFiles?: boolean;
- poll?: boolean;
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
/** Format: misskey:id */
- untilId?: string;
+ noteId: string;
};
};
};
@@ -23059,7 +24304,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Note'][];
+ 'application/json': components['schemas']['Clip'][];
};
};
/** @description Client error */
@@ -23101,12 +24346,12 @@ export type operations = {
};
};
/**
- * notes/children
+ * notes/conversation
* @description No description provided.
*
* **Credential required**: *No*
*/
- notes___children: {
+ notes___conversation: {
requestBody: {
content: {
'application/json': {
@@ -23114,12 +24359,8 @@ export type operations = {
noteId: string;
/** @default 10 */
limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- /** @default true */
- showQuotes?: boolean;
+ /** @default 0 */
+ offset?: number;
};
};
};
@@ -23169,17 +24410,50 @@ export type operations = {
};
};
/**
- * notes/clips
+ * notes/create
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
*/
- notes___clips: {
+ notes___create: {
requestBody: {
content: {
'application/json': {
+ /**
+ * @default public
+ * @enum {string}
+ */
+ visibility?: 'public' | 'home' | 'followers' | 'specified';
+ visibleUserIds?: string[];
+ cw?: string | null;
+ /** @default false */
+ localOnly?: boolean;
+ /**
+ * @default null
+ * @enum {string|null}
+ */
+ reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
+ /** @default false */
+ noExtractMentions?: boolean;
+ /** @default false */
+ noExtractHashtags?: boolean;
+ /** @default false */
+ noExtractEmojis?: boolean;
/** Format: misskey:id */
- noteId: string;
+ replyId?: string | null;
+ /** Format: misskey:id */
+ renoteId?: string | null;
+ /** Format: misskey:id */
+ channelId?: string | null;
+ text?: string | null;
+ fileIds?: string[];
+ mediaIds?: string[];
+ poll?: ({
+ choices: string[];
+ multiple?: boolean;
+ expiresAt?: number | null;
+ expiredAfter?: number | null;
+ }) | null;
};
};
};
@@ -23187,7 +24461,9 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Clip'][];
+ 'application/json': {
+ createdNote: components['schemas']['Note'];
+ };
};
};
/** @description Client error */
@@ -23229,30 +24505,24 @@ export type operations = {
};
};
/**
- * notes/conversation
+ * notes/delete
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:notes*
*/
- notes___conversation: {
+ notes___delete: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
noteId: string;
- /** @default 10 */
- limit?: number;
- /** @default 0 */
- offset?: number;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Note'][];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -23293,15 +24563,17 @@ export type operations = {
};
};
/**
- * notes/create
+ * notes/edit
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:notes*
*/
- notes___create: {
+ notes___edit: {
requestBody: {
content: {
'application/json': {
+ /** Format: misskey:id */
+ editId?: string;
/**
* @default public
* @enum {string}
@@ -23388,64 +24660,6 @@ export type operations = {
};
};
/**
- * notes/delete
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:notes*
- */
- notes___delete: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- noteId: string;
- };
- };
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
* notes/favorites/create
* @description No description provided.
*
@@ -23779,21 +24993,15 @@ export type operations = {
};
};
/**
- * notes/bubble-timeline
+ * notes/hybrid-timeline
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- 'notes___bubble-timeline': {
+ 'notes___hybrid-timeline': {
requestBody: {
content: {
'application/json': {
- /** @default false */
- withFiles?: boolean;
- /** @default true */
- withBots?: boolean;
- /** @default true */
- withRenotes?: boolean;
/** @default 10 */
limit?: number;
/** Format: misskey:id */
@@ -23802,6 +25010,22 @@ export type operations = {
untilId?: string;
sinceDate?: number;
untilDate?: number;
+ /** @default false */
+ allowPartial?: boolean;
+ /** @default true */
+ includeMyRenotes?: boolean;
+ /** @default true */
+ includeRenotedMyNotes?: boolean;
+ /** @default true */
+ includeLocalRenotes?: boolean;
+ /** @default false */
+ withFiles?: boolean;
+ /** @default true */
+ withRenotes?: boolean;
+ /** @default false */
+ withReplies?: boolean;
+ /** @default true */
+ withBots?: boolean;
};
};
};
@@ -23851,48 +25075,25 @@ export type operations = {
};
};
/**
- * notes/hybrid-timeline
+ * notes/like
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:reactions*
*/
- 'notes___hybrid-timeline': {
+ notes___like: {
requestBody: {
content: {
'application/json': {
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
/** Format: misskey:id */
- untilId?: string;
- sinceDate?: number;
- untilDate?: number;
- /** @default false */
- allowPartial?: boolean;
- /** @default true */
- includeMyRenotes?: boolean;
- /** @default true */
- includeRenotedMyNotes?: boolean;
- /** @default true */
- includeLocalRenotes?: boolean;
- /** @default false */
- withFiles?: boolean;
- /** @default true */
- withRenotes?: boolean;
- /** @default false */
- withReplies?: boolean;
- /** @default true */
- withBots?: boolean;
+ noteId: string;
+ override?: string | null;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Note'][];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -24140,18 +25341,17 @@ export type operations = {
};
};
/**
- * notes/polls/vote
+ * notes/polls/refresh
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:votes*
+ * **Credential required**: *Yes* / **Permission**: *read:federation*
*/
- notes___polls___vote: {
+ notes___polls___refresh: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
noteId: string;
- choice: number;
};
};
};
@@ -24199,17 +25399,18 @@ export type operations = {
};
};
/**
- * notes/polls/refresh
+ * notes/polls/vote
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:federation*
+ * **Credential required**: *Yes* / **Permission**: *write:votes*
*/
- notes___polls___refresh: {
+ notes___polls___vote: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
noteId: string;
+ choice: number;
};
};
};
@@ -24441,65 +25642,6 @@ export type operations = {
};
};
/**
- * notes/like
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:reactions*
- */
- notes___like: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- noteId: string;
- override?: string | null;
- };
- };
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
* notes/renotes
* @description No description provided.
*
@@ -24869,35 +26011,39 @@ export type operations = {
};
};
/**
- * notes/search-by-tag
+ * notes/search
* @description No description provided.
*
* **Credential required**: *No*
*/
- 'notes___search-by-tag': {
+ notes___search: {
requestBody: {
content: {
'application/json': {
- /** @default null */
- reply?: boolean | null;
- /** @default null */
- renote?: boolean | null;
- /**
- * @description Only show notes that have attached files.
- * @default false
- */
- withFiles?: boolean;
- /** @default null */
- poll?: boolean | null;
+ query: string;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
/** @default 10 */
limit?: number;
- tag?: string;
- /** @description The outer arrays are chained with OR, the inner arrays are chained with AND. */
- query?: string[][];
+ /** @default 0 */
+ offset?: number;
+ /** @description The local host is represented with `.`. */
+ host?: string;
+ /** @enum {string|null} */
+ filetype?: 'image' | 'video' | 'audio' | 'module' | 'flash' | null;
+ /**
+ * Format: misskey:id
+ * @default null
+ */
+ userId?: string | null;
+ /**
+ * Format: misskey:id
+ * @default null
+ */
+ channelId?: string | null;
+ order?: string;
};
};
};
@@ -24947,38 +26093,35 @@ export type operations = {
};
};
/**
- * notes/search
+ * notes/search-by-tag
* @description No description provided.
*
* **Credential required**: *No*
*/
- notes___search: {
+ 'notes___search-by-tag': {
requestBody: {
content: {
'application/json': {
- query: string;
+ /** @default null */
+ reply?: boolean | null;
+ /** @default null */
+ renote?: boolean | null;
+ /**
+ * @description Only show notes that have attached files.
+ * @default false
+ */
+ withFiles?: boolean;
+ /** @default null */
+ poll?: boolean | null;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
/** @default 10 */
limit?: number;
- /** @default 0 */
- offset?: number;
- /** @description The local host is represented with `.`. */
- host?: string;
- filetype?: string | null;
- /**
- * Format: misskey:id
- * @default null
- */
- userId?: string | null;
- /**
- * Format: misskey:id
- * @default null
- */
- channelId?: string | null;
- order?: string;
+ tag?: string;
+ /** @description The outer arrays are chained with OR, the inner arrays are chained with AND. */
+ query?: string[][];
};
};
};
@@ -25558,103 +26701,6 @@ export type operations = {
};
};
/**
- * notes/edit
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:notes*
- */
- notes___edit: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- editId?: string;
- /**
- * @default public
- * @enum {string}
- */
- visibility?: 'public' | 'home' | 'followers' | 'specified';
- visibleUserIds?: string[];
- cw?: string | null;
- /** @default false */
- localOnly?: boolean;
- /**
- * @default null
- * @enum {string|null}
- */
- reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
- /** @default false */
- noExtractMentions?: boolean;
- /** @default false */
- noExtractHashtags?: boolean;
- /** @default false */
- noExtractEmojis?: boolean;
- /** Format: misskey:id */
- replyId?: string | null;
- /** Format: misskey:id */
- renoteId?: string | null;
- /** Format: misskey:id */
- channelId?: string | null;
- text?: string | null;
- fileIds?: string[];
- mediaIds?: string[];
- poll?: ({
- choices: string[];
- multiple?: boolean;
- expiresAt?: number | null;
- expiredAfter?: number | null;
- }) | null;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- createdNote: components['schemas']['Note'];
- };
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
* notes/versions
* @description No description provided.
*
@@ -26426,32 +27472,71 @@ export type operations = {
};
};
/**
- * flash/create
+ * ping
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *No*
*/
- flash___create: {
- requestBody: {
- content: {
- 'application/json': {
- title: string;
- summary: string;
- script: string;
- permissions: string[];
- /**
- * @default public
- * @enum {string}
- */
- visibility?: 'public' | 'private';
+ ping: {
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ pong: number;
+ };
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
};
};
};
+ };
+ /**
+ * pinned-users
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ 'pinned-users': {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Flash'];
+ 'application/json': components['schemas']['UserDetailed'][];
};
};
/** @description Client error */
@@ -26493,17 +27578,17 @@ export type operations = {
};
};
/**
- * flash/delete
+ * promo/read
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- flash___delete: {
+ promo___read: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- flashId: string;
+ noteId: string;
};
};
};
@@ -26551,28 +27636,24 @@ export type operations = {
};
};
/**
- * flash/featured
+ * renote-mute/create
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
- flash___featured: {
+ 'renote-mute___create': {
requestBody: {
content: {
'application/json': {
- /** @default 0 */
- offset?: number;
- /** @default 10 */
- limit?: number;
+ /** Format: misskey:id */
+ userId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Flash'][];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -26613,17 +27694,17 @@ export type operations = {
};
};
/**
- * flash/like
+ * renote-mute/delete
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ * **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
- flash___like: {
+ 'renote-mute___delete': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- flashId: string;
+ userId: string;
};
};
};
@@ -26671,17 +27752,21 @@ export type operations = {
};
};
/**
- * flash/show
+ * renote-mute/list
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:mutes*
*/
- flash___show: {
+ 'renote-mute___list': {
requestBody: {
content: {
'application/json': {
+ /** @default 30 */
+ limit?: number;
/** Format: misskey:id */
- flashId: string;
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
};
};
};
@@ -26689,7 +27774,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Flash'];
+ 'application/json': components['schemas']['RenoteMuting'][];
};
};
/** @description Client error */
@@ -26731,17 +27816,17 @@ export type operations = {
};
};
/**
- * flash/unlike
- * @description No description provided.
+ * request-reset-password
+ * @description Request a users password to be reset.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash-likes*
+ * **Credential required**: *No*
*/
- flash___unlike: {
+ 'request-reset-password': {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- flashId: string;
+ username: string;
+ email: string;
};
};
};
@@ -26789,23 +27874,67 @@ export type operations = {
};
};
/**
- * flash/update
- * @description No description provided.
+ * reset-db
+ * @description Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis.
*
- * **Credential required**: *Yes* / **Permission**: *write:flash*
+ * **Credential required**: *No*
*/
- flash___update: {
+ 'reset-db': {
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * reset-password
+ * @description Complete the password reset that was previously requested.
+ *
+ * **Credential required**: *No*
+ */
+ 'reset-password': {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- flashId: string;
- title?: string;
- summary?: string;
- script?: string;
- permissions?: string[];
- /** @enum {string} */
- visibility?: 'public' | 'private';
+ token: string;
+ password: string;
};
};
};
@@ -26853,29 +27982,24 @@ export type operations = {
};
};
/**
- * flash/my
+ * retention
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:flash*
+ * **Credential required**: *No*
*/
- flash___my: {
- requestBody: {
- content: {
- 'application/json': {
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- };
- };
- };
+ retention: {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Flash'][];
+ 'application/json': {
+ /** Format: date-time */
+ createdAt: string;
+ users: number;
+ data: {
+ [key: string]: number;
+ };
+ }[];
};
};
/** @description Client error */
@@ -26917,34 +28041,24 @@ export type operations = {
};
};
/**
- * flash/my-likes
+ * reversi/cancel-match
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:flash-likes*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- 'flash___my-likes': {
+ 'reversi___cancel-match': {
requestBody: {
content: {
'application/json': {
- /** @default 10 */
- limit?: number;
/** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
+ userId?: string | null;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- /** Format: id */
- id: string;
- flash: components['schemas']['Flash'];
- }[];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -26985,19 +28099,31 @@ export type operations = {
};
};
/**
- * ping
+ * reversi/games
* @description No description provided.
*
* **Credential required**: *No*
*/
- ping: {
+ reversi___games: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 10 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ /** @default false */
+ my?: boolean;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- pong: number;
- };
+ 'application/json': components['schemas']['ReversiGameLite'][];
};
};
/** @description Client error */
@@ -27039,17 +28165,17 @@ export type operations = {
};
};
/**
- * pinned-users
+ * reversi/invitations
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- 'pinned-users': {
+ reversi___invitations: {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['UserDetailed'][];
+ 'application/json': components['schemas']['UserLite'][];
};
};
/** @description Client error */
@@ -27091,21 +28217,31 @@ export type operations = {
};
};
/**
- * promo/read
+ * reversi/match
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- promo___read: {
+ reversi___match: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- noteId: string;
+ userId?: string | null;
+ /** @default false */
+ noIrregularRules?: boolean;
+ /** @default false */
+ multiple?: boolean;
};
};
};
responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['ReversiGameDetailed'];
+ };
+ };
/** @description OK (without any results) */
204: {
content: never;
@@ -27149,17 +28285,25 @@ export type operations = {
};
};
/**
- * roles/list
+ * reversi/show-game
* @description No description provided.
*
- * **Credential required**: *Yes* / **Permission**: *read:account*
+ * **Credential required**: *No*
*/
- roles___list: {
+ 'reversi___show-game': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ gameId: string;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Role'][];
+ 'application/json': components['schemas']['ReversiGameDetailed'];
};
};
/** @description Client error */
@@ -27201,26 +28345,24 @@ export type operations = {
};
};
/**
- * roles/show
+ * reversi/surrender
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- roles___show: {
+ reversi___surrender: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- roleId: string;
+ gameId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Role'];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -27261,23 +28403,18 @@ export type operations = {
};
};
/**
- * roles/users
+ * reversi/verify
* @description No description provided.
*
* **Credential required**: *No*
*/
- roles___users: {
+ reversi___verify: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- roleId: string;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- /** @default 10 */
- limit?: number;
+ gameId: string;
+ crc32: string;
};
};
};
@@ -27286,10 +28423,9 @@ export type operations = {
200: {
content: {
'application/json': {
- /** Format: misskey:id */
- id: string;
- user: components['schemas']['UserDetailed'];
- }[];
+ desynced: boolean;
+ game?: components['schemas']['ReversiGameDetailed'] | null;
+ };
};
};
/** @description Client error */
@@ -27331,33 +28467,17 @@ export type operations = {
};
};
/**
- * roles/notes
+ * roles/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- roles___notes: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- roleId: string;
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- sinceDate?: number;
- untilDate?: number;
- };
- };
- };
+ roles___list: {
responses: {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Note'][];
+ 'application/json': components['schemas']['Role'][];
};
};
/** @description Client error */
@@ -27399,24 +28519,34 @@ export type operations = {
};
};
/**
- * request-reset-password
- * @description Request a users password to be reset.
+ * roles/notes
+ * @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:account*
*/
- 'request-reset-password': {
+ roles___notes: {
requestBody: {
content: {
'application/json': {
- username: string;
- email: string;
+ /** Format: misskey:id */
+ roleId: string;
+ /** @default 10 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ sinceDate?: number;
+ untilDate?: number;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Note'][];
+ };
};
/** @description Client error */
400: {
@@ -27457,16 +28587,26 @@ export type operations = {
};
};
/**
- * reset-db
- * @description Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis.
+ * roles/show
+ * @description No description provided.
*
* **Credential required**: *No*
*/
- 'reset-db': {
+ roles___show: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ roleId: string;
+ };
+ };
+ };
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Role'];
+ };
};
/** @description Client error */
400: {
@@ -27507,24 +28647,36 @@ export type operations = {
};
};
/**
- * reset-password
- * @description Complete the password reset that was previously requested.
+ * roles/users
+ * @description No description provided.
*
* **Credential required**: *No*
*/
- 'reset-password': {
+ roles___users: {
requestBody: {
content: {
'application/json': {
- token: string;
- password: string;
+ /** Format: misskey:id */
+ roleId: string;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ /** @default 10 */
+ limit?: number;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ user: components['schemas']['UserDetailed'];
+ }[];
+ };
};
/** @description Client error */
400: {
@@ -27630,6 +28782,66 @@ export type operations = {
};
};
/**
+ * sponsors
+ * @description Get Sharkey Sponsors or Instance Sponsors
+ *
+ * **Credential required**: *No*
+ */
+ sponsors: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default false */
+ forceUpdate?: boolean;
+ /** @default false */
+ instance?: boolean;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* stats
* @description No description provided.
*
@@ -27690,17 +28902,21 @@ export type operations = {
};
};
/**
- * sw/show-registration
- * @description Check push notification registration exists.
+ * sw/register
+ * @description Register to receive push notifications.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'sw___show-registration': {
+ sw___register: {
requestBody: {
content: {
'application/json': {
endpoint: string;
+ auth: string;
+ publickey: string;
+ /** @default false */
+ sendReadMessage?: boolean;
};
};
};
@@ -27709,16 +28925,15 @@ export type operations = {
200: {
content: {
'application/json': {
+ /** @enum {string} */
+ state?: 'already-subscribed' | 'subscribed';
+ key: string | null;
userId: string;
endpoint: string;
sendReadMessage: boolean;
- } | null;
+ };
};
};
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
/** @description Client error */
400: {
content: {
@@ -27758,18 +28973,17 @@ export type operations = {
};
};
/**
- * sw/update-registration
- * @description Update push notification registration.
+ * sw/show-registration
+ * @description Check push notification registration exists.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
- 'sw___update-registration': {
+ 'sw___show-registration': {
requestBody: {
content: {
'application/json': {
endpoint: string;
- sendReadMessage?: boolean;
};
};
};
@@ -27781,9 +28995,13 @@ export type operations = {
userId: string;
endpoint: string;
sendReadMessage: boolean;
- };
+ } | null;
};
};
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
/** @description Client error */
400: {
content: {
@@ -27823,37 +29041,23 @@ export type operations = {
};
};
/**
- * sw/register
- * @description Register to receive push notifications.
+ * sw/unregister
+ * @description Unregister from receiving push notifications.
*
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
+ * **Credential required**: *No*
*/
- sw___register: {
+ sw___unregister: {
requestBody: {
content: {
'application/json': {
endpoint: string;
- auth: string;
- publickey: string;
- /** @default false */
- sendReadMessage?: boolean;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- /** @enum {string} */
- state?: 'already-subscribed' | 'subscribed';
- key: string | null;
- userId: string;
- endpoint: string;
- sendReadMessage: boolean;
- };
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -27894,23 +29098,31 @@ export type operations = {
};
};
/**
- * sw/unregister
- * @description Unregister from receiving push notifications.
+ * sw/update-registration
+ * @description Update push notification registration.
*
- * **Credential required**: *No*
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes*
*/
- sw___unregister: {
+ 'sw___update-registration': {
requestBody: {
content: {
'application/json': {
endpoint: string;
+ sendReadMessage?: boolean;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ userId: string;
+ endpoint: string;
+ sendReadMessage: boolean;
+ };
+ };
};
/** @description Client error */
400: {
@@ -28165,6 +29377,69 @@ export type operations = {
};
};
/**
+ * users/achievements
+ * @description No description provided.
+ *
+ * **Credential required**: *No*
+ */
+ users___achievements: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ userId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ name: string;
+ unlockedAt: number;
+ }[];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Too many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
* users/clips
* @description Show all clips this user owns.
*
@@ -28231,26 +29506,21 @@ export type operations = {
};
};
/**
- * users/followers
- * @description Show everyone that follows this user.
+ * users/featured-notes
+ * @description No description provided.
*
* **Credential required**: *No*
*/
- users___followers: {
+ 'users___featured-notes': {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
/** @default 10 */
limit?: number;
/** Format: misskey:id */
- userId?: string;
- username?: string;
- /** @description The local host is represented with `null`. */
- host?: string | null;
+ untilId?: string;
+ /** Format: misskey:id */
+ userId: string;
};
};
};
@@ -28258,7 +29528,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Following'][];
+ 'application/json': components['schemas']['Note'][];
};
};
/** @description Client error */
@@ -28300,27 +29570,23 @@ export type operations = {
};
};
/**
- * users/following
- * @description Show everyone that this user is following.
+ * users/flashs
+ * @description Show all flashs this user created.
*
* **Credential required**: *No*
*/
- users___following: {
+ users___flashs: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
+ userId: string;
/** @default 10 */
limit?: number;
/** Format: misskey:id */
- userId?: string;
- username?: string;
- /** @description The local host is represented with `null`. */
- host?: string | null;
- birthday?: string | null;
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
};
};
};
@@ -28328,7 +29594,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Following'][];
+ 'application/json': components['schemas']['Flash'][];
};
};
/** @description Client error */
@@ -28370,23 +29636,26 @@ export type operations = {
};
};
/**
- * users/gallery/posts
- * @description Show all gallery posts by the given user.
+ * users/followers
+ * @description Show everyone that follows this user.
*
* **Credential required**: *No*
*/
- users___gallery___posts: {
+ users___followers: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- userId: string;
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
+ /** @default 10 */
+ limit?: number;
+ /** Format: misskey:id */
+ userId?: string;
+ username?: string;
+ /** @description The local host is represented with `null`. */
+ host?: string | null;
};
};
};
@@ -28394,7 +29663,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['GalleryPost'][];
+ 'application/json': components['schemas']['Following'][];
};
};
/** @description Client error */
@@ -28436,19 +29705,27 @@ export type operations = {
};
};
/**
- * users/get-frequently-replied-users
- * @description Get a list of other users that the specified user frequently replies to.
+ * users/following
+ * @description Show everyone that this user is following.
*
* **Credential required**: *No*
*/
- 'users___get-frequently-replied-users': {
+ users___following: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- userId: string;
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
/** @default 10 */
limit?: number;
+ /** Format: misskey:id */
+ userId?: string;
+ username?: string;
+ /** @description The local host is represented with `null`. */
+ host?: string | null;
+ birthday?: string | null;
};
};
};
@@ -28456,10 +29733,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- user: components['schemas']['UserDetailed'];
- weight: number;
- }[];
+ 'application/json': components['schemas']['Following'][];
};
};
/** @description Client error */
@@ -28501,21 +29775,23 @@ export type operations = {
};
};
/**
- * users/featured-notes
- * @description No description provided.
+ * users/gallery/posts
+ * @description Show all gallery posts by the given user.
*
* **Credential required**: *No*
*/
- 'users___featured-notes': {
+ users___gallery___posts: {
requestBody: {
content: {
'application/json': {
+ /** Format: misskey:id */
+ userId: string;
/** @default 10 */
limit?: number;
/** Format: misskey:id */
- untilId?: string;
+ sinceId?: string;
/** Format: misskey:id */
- userId: string;
+ untilId?: string;
};
};
};
@@ -28523,7 +29799,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Note'][];
+ 'application/json': components['schemas']['GalleryPost'][];
};
};
/** @description Client error */
@@ -28565,16 +29841,19 @@ export type operations = {
};
};
/**
- * users/lists/create
- * @description Create a new list of users.
+ * users/get-frequently-replied-users
+ * @description Get a list of other users that the specified user frequently replies to.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *No*
*/
- users___lists___create: {
+ 'users___get-frequently-replied-users': {
requestBody: {
content: {
'application/json': {
- name: string;
+ /** Format: misskey:id */
+ userId: string;
+ /** @default 10 */
+ limit?: number;
};
};
};
@@ -28582,7 +29861,10 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['UserList'];
+ 'application/json': {
+ user: components['schemas']['UserDetailed'];
+ weight: number;
+ }[];
};
};
/** @description Client error */
@@ -28624,24 +29906,25 @@ export type operations = {
};
};
/**
- * users/lists/delete
- * @description Delete an existing list of users.
+ * users/lists/create
+ * @description Create a new list of users.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- users___lists___delete: {
+ users___lists___create: {
requestBody: {
content: {
'application/json': {
- /** Format: misskey:id */
- listId: string;
+ name: string;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['UserList'];
+ };
};
/** @description Client error */
400: {
@@ -28682,17 +29965,18 @@ export type operations = {
};
};
/**
- * users/lists/list
- * @description Show all lists that the authenticated user has created.
+ * users/lists/create-from-public
+ * @description No description provided.
*
- * **Credential required**: *No* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- users___lists___list: {
+ 'users___lists___create-from-public': {
requestBody: {
content: {
'application/json': {
+ name: string;
/** Format: misskey:id */
- userId?: string;
+ listId: string;
};
};
};
@@ -28700,7 +29984,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['UserList'][];
+ 'application/json': components['schemas']['UserList'];
};
};
/** @description Client error */
@@ -28742,19 +30026,17 @@ export type operations = {
};
};
/**
- * users/lists/pull
- * @description Remove a user from a list.
+ * users/lists/delete
+ * @description Delete an existing list of users.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- users___lists___pull: {
+ users___lists___delete: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
listId: string;
- /** Format: misskey:id */
- userId: string;
};
};
};
@@ -28802,19 +30084,17 @@ export type operations = {
};
};
/**
- * users/lists/push
- * @description Add a user to an existing list.
+ * users/lists/favorite
+ * @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- users___lists___push: {
+ users___lists___favorite: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
listId: string;
- /** Format: misskey:id */
- userId: string;
};
};
};
@@ -28862,12 +30142,12 @@ export type operations = {
};
};
/**
- * users/lists/show
- * @description Show the properties of a list.
+ * users/lists/get-memberships
+ * @description No description provided.
*
* **Credential required**: *No* / **Permission**: *read:account*
*/
- users___lists___show: {
+ 'users___lists___get-memberships': {
requestBody: {
content: {
'application/json': {
@@ -28875,6 +30155,12 @@ export type operations = {
listId: string;
/** @default false */
forPublic?: boolean;
+ /** @default 30 */
+ limit?: number;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
};
};
};
@@ -28882,7 +30168,16 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['UserList'];
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ /** Format: misskey:id */
+ userId: string;
+ user: components['schemas']['UserLite'];
+ withReplies: boolean;
+ }[];
};
};
/** @description Client error */
@@ -28924,24 +30219,26 @@ export type operations = {
};
};
/**
- * users/lists/favorite
- * @description No description provided.
+ * users/lists/list
+ * @description Show all lists that the authenticated user has created.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *No* / **Permission**: *read:account*
*/
- users___lists___favorite: {
+ users___lists___list: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
- listId: string;
+ userId?: string;
};
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['UserList'][];
+ };
};
/** @description Client error */
400: {
@@ -28982,17 +30279,19 @@ export type operations = {
};
};
/**
- * users/lists/unfavorite
- * @description No description provided.
+ * users/lists/pull
+ * @description Remove a user from a list.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- users___lists___unfavorite: {
+ users___lists___pull: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
listId: string;
+ /** Format: misskey:id */
+ userId: string;
};
};
};
@@ -29040,28 +30339,26 @@ export type operations = {
};
};
/**
- * users/lists/update
- * @description Update the properties of a list.
+ * users/lists/push
+ * @description Add a user to an existing list.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- users___lists___update: {
+ users___lists___push: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
listId: string;
- name?: string;
- isPublic?: boolean;
+ /** Format: misskey:id */
+ userId: string;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['UserList'];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -29102,18 +30399,19 @@ export type operations = {
};
};
/**
- * users/lists/create-from-public
- * @description No description provided.
+ * users/lists/show
+ * @description Show the properties of a list.
*
- * **Credential required**: *Yes* / **Permission**: *write:account*
+ * **Credential required**: *No* / **Permission**: *read:account*
*/
- 'users___lists___create-from-public': {
+ users___lists___show: {
requestBody: {
content: {
'application/json': {
- name: string;
/** Format: misskey:id */
listId: string;
+ /** @default false */
+ forPublic?: boolean;
};
};
};
@@ -29163,20 +30461,17 @@ export type operations = {
};
};
/**
- * users/lists/update-membership
+ * users/lists/unfavorite
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
- 'users___lists___update-membership': {
+ users___lists___unfavorite: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
listId: string;
- /** Format: misskey:id */
- userId: string;
- withReplies?: boolean;
};
};
};
@@ -29224,25 +30519,19 @@ export type operations = {
};
};
/**
- * users/lists/get-memberships
- * @description No description provided.
+ * users/lists/update
+ * @description Update the properties of a list.
*
- * **Credential required**: *No* / **Permission**: *read:account*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- 'users___lists___get-memberships': {
+ users___lists___update: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
listId: string;
- /** @default false */
- forPublic?: boolean;
- /** @default 30 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
+ name?: string;
+ isPublic?: boolean;
};
};
};
@@ -29250,16 +30539,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': {
- /** Format: misskey:id */
- id: string;
- /** Format: date-time */
- createdAt: string;
- /** Format: misskey:id */
- userId: string;
- user: components['schemas']['UserLite'];
- withReplies: boolean;
- }[];
+ 'application/json': components['schemas']['UserList'];
};
};
/** @description Client error */
@@ -29301,52 +30581,27 @@ export type operations = {
};
};
/**
- * users/notes
+ * users/lists/update-membership
* @description No description provided.
*
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *write:account*
*/
- users___notes: {
+ 'users___lists___update-membership': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
+ listId: string;
+ /** Format: misskey:id */
userId: string;
- /** @default false */
withReplies?: boolean;
- /** @default true */
- withRepliesToSelf?: boolean;
- /** @default true */
- withQuotes?: boolean;
- /** @default true */
- withRenotes?: boolean;
- /** @default true */
- withBots?: boolean;
- /** @default true */
- withNonPublic?: boolean;
- /** @default false */
- withChannelNotes?: boolean;
- /** @default 10 */
- limit?: number;
- /** Format: misskey:id */
- sinceId?: string;
- /** Format: misskey:id */
- untilId?: string;
- sinceDate?: number;
- untilDate?: number;
- /** @default false */
- allowPartial?: boolean;
- /** @default false */
- withFiles?: boolean;
};
};
};
responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['Note'][];
- };
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
};
/** @description Client error */
400: {
@@ -29387,23 +30642,43 @@ export type operations = {
};
};
/**
- * users/pages
- * @description Show all pages this user created.
+ * users/notes
+ * @description No description provided.
*
* **Credential required**: *No*
*/
- users___pages: {
+ users___notes: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
+ /** @default false */
+ withReplies?: boolean;
+ /** @default true */
+ withRepliesToSelf?: boolean;
+ /** @default true */
+ withQuotes?: boolean;
+ /** @default true */
+ withRenotes?: boolean;
+ /** @default true */
+ withBots?: boolean;
+ /** @default true */
+ withNonPublic?: boolean;
+ /** @default false */
+ withChannelNotes?: boolean;
/** @default 10 */
limit?: number;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
+ sinceDate?: number;
+ untilDate?: number;
+ /** @default false */
+ allowPartial?: boolean;
+ /** @default false */
+ withFiles?: boolean;
};
};
};
@@ -29411,7 +30686,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Page'][];
+ 'application/json': components['schemas']['Note'][];
};
};
/** @description Client error */
@@ -29453,12 +30728,12 @@ export type operations = {
};
};
/**
- * users/flashs
- * @description Show all flashs this user created.
+ * users/pages
+ * @description Show all pages this user created.
*
* **Credential required**: *No*
*/
- users___flashs: {
+ users___pages: {
requestBody: {
content: {
'application/json': {
@@ -29477,7 +30752,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
- 'application/json': components['schemas']['Flash'][];
+ 'application/json': components['schemas']['Page'][];
};
};
/** @description Client error */
@@ -29677,7 +30952,9 @@ export type operations = {
isBlocked: boolean;
isMuted: boolean;
isRenoteMuted: boolean;
- }, {
+ isInstanceMuted?: boolean;
+ memo?: string | null;
+ }, ({
/** Format: id */
id: string;
isFollowing: boolean;
@@ -29688,7 +30965,9 @@ export type operations = {
isBlocked: boolean;
isMuted: boolean;
isRenoteMuted: boolean;
- }[]]>;
+ isInstanceMuted?: boolean;
+ memo?: string | null;
+ })[]]>;
};
};
/** @description Client error */
@@ -29789,21 +31068,27 @@ export type operations = {
};
};
/**
- * users/search-by-username-and-host
- * @description Search for a user by username and/or host.
+ * users/search
+ * @description Search for users.
*
* **Credential required**: *No*
*/
- 'users___search-by-username-and-host': {
+ users___search: {
requestBody: {
content: {
'application/json': {
+ query: string;
+ /** @default 0 */
+ offset?: number;
/** @default 10 */
limit?: number;
+ /**
+ * @default combined
+ * @enum {string}
+ */
+ origin?: 'local' | 'remote' | 'combined';
/** @default true */
detail?: boolean;
- username?: string | null;
- host?: string | null;
};
};
};
@@ -29853,27 +31138,21 @@ export type operations = {
};
};
/**
- * users/search
- * @description Search for users.
+ * users/search-by-username-and-host
+ * @description Search for a user by username and/or host.
*
* **Credential required**: *No*
*/
- users___search: {
+ 'users___search-by-username-and-host': {
requestBody: {
content: {
'application/json': {
- query: string;
- /** @default 0 */
- offset?: number;
/** @default 10 */
limit?: number;
- /**
- * @default combined
- * @enum {string}
- */
- origin?: 'local' | 'remote' | 'combined';
/** @default true */
detail?: boolean;
+ username?: string | null;
+ host?: string | null;
};
};
};
@@ -29987,69 +31266,6 @@ export type operations = {
};
};
/**
- * users/achievements
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- users___achievements: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- userId: string;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- name: string;
- unlockedAt: number;
- }[];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
* users/update-memo
* @description No description provided.
*
@@ -30110,794 +31326,49 @@ export type operations = {
};
};
/**
- * fetch-rss
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- 'fetch-rss': {
- requestBody: {
- content: {
- 'application/json': {
- url: string;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- image?: {
- link?: string;
- url: string;
- title?: string;
- };
- paginationLinks?: {
- self?: string;
- first?: string;
- next?: string;
- last?: string;
- prev?: string;
- };
- link?: string;
- title?: string;
- items: {
- link?: string;
- guid?: string;
- title?: string;
- pubDate?: string;
- creator?: string;
- summary?: string;
- content?: string;
- isoDate?: string;
- categories?: string[];
- contentSnippet?: string;
- enclosure?: {
- url: string;
- length?: number;
- type?: string;
- };
- }[];
- feedUrl?: string;
- description?: string;
- itunes?: {
- image?: string;
- owner?: {
- name?: string;
- email?: string;
- };
- author?: string;
- summary?: string;
- explicit?: string;
- categories?: string[];
- keywords?: string[];
- [key: string]: unknown;
- };
- };
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * fetch-external-resources
- * @description No description provided.
- *
- * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
- * **Credential required**: *Yes*
- */
- 'fetch-external-resources': {
- requestBody: {
- content: {
- 'application/json': {
- url: string;
- hash: string;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- type: string;
- data: string;
- };
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * retention
+ * v2/admin/emoji/list
* @description No description provided.
*
- * **Credential required**: *No*
- */
- retention: {
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- /** Format: date-time */
- createdAt: string;
- users: number;
- data: {
- [key: string]: number;
- };
- }[];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * sponsors
- * @description Get Sharkey Sponsors or Instance Sponsors
- *
- * **Credential required**: *No*
- */
- sponsors: {
- requestBody: {
- content: {
- 'application/json': {
- /** @default false */
- forceUpdate?: boolean;
- /** @default false */
- instance?: boolean;
- };
- };
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * bubble-game/register
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- 'bubble-game___register': {
- requestBody: {
- content: {
- 'application/json': {
- score: number;
- seed: string;
- logs: number[][];
- gameMode: string;
- gameVersion: number;
- };
- };
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * bubble-game/ranking
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- 'bubble-game___ranking': {
- requestBody: {
- content: {
- 'application/json': {
- gameMode: string;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- id: string;
- score: number;
- user?: components['schemas']['UserLite'];
- }[];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * reversi/cancel-match
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- 'reversi___cancel-match': {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- userId?: string | null;
- };
- };
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * reversi/games
- * @description No description provided.
- *
- * **Credential required**: *No*
+ * **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
- reversi___games: {
+ v2___admin___emoji___list: {
requestBody: {
content: {
'application/json': {
- /** @default 10 */
- limit?: number;
+ query?: ({
+ updatedAtFrom?: string;
+ updatedAtTo?: string;
+ name?: string;
+ host?: string;
+ uri?: string;
+ publicUrl?: string;
+ originalUrl?: string;
+ type?: string;
+ aliases?: string;
+ category?: string;
+ license?: string;
+ isSensitive?: boolean;
+ localOnly?: boolean;
+ /**
+ * @default all
+ * @enum {string}
+ */
+ hostType?: 'local' | 'remote' | 'all';
+ roleIds?: string[];
+ }) | null;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
- /** @default false */
- my?: boolean;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['ReversiGameLite'][];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * reversi/match
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- reversi___match: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- userId?: string | null;
- /** @default false */
- noIrregularRules?: boolean;
- /** @default false */
- multiple?: boolean;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['ReversiGameDetailed'];
- };
- };
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * reversi/invitations
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *read:account*
- */
- reversi___invitations: {
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['UserLite'][];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * reversi/show-game
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- 'reversi___show-game': {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- gameId: string;
- };
- };
- };
- responses: {
- /** @description OK (with results) */
- 200: {
- content: {
- 'application/json': components['schemas']['ReversiGameDetailed'];
- };
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * reversi/surrender
- * @description No description provided.
- *
- * **Credential required**: *Yes* / **Permission**: *write:account*
- */
- reversi___surrender: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- gameId: string;
- };
- };
- };
- responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
- };
- /** @description Client error */
- 400: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Authentication error */
- 401: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Forbidden error */
- 403: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description I'm Ai */
- 418: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- /** @description Internal server error */
- 500: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
- };
- };
- /**
- * reversi/verify
- * @description No description provided.
- *
- * **Credential required**: *No*
- */
- reversi___verify: {
- requestBody: {
- content: {
- 'application/json': {
- /** Format: misskey:id */
- gameId: string;
- crc32: string;
+ /** @default 10 */
+ limit?: number;
+ page?: number;
+ /**
+ * @default [
+ * "-id"
+ * ]
+ */
+ sortKeys?: ('+id' | '-id' | '+updatedAt' | '-updatedAt' | '+name' | '-name' | '+host' | '-host' | '+uri' | '-uri' | '+publicUrl' | '-publicUrl' | '+type' | '-type' | '+aliases' | '-aliases' | '+category' | '-category' | '+license' | '-license' | '+isSensitive' | '-isSensitive' | '+localOnly' | '-localOnly' | '+roleIdsThatCanBeUsedThisEmojiAsReaction' | '-roleIdsThatCanBeUsedThisEmojiAsReaction')[];
};
};
};
@@ -30906,8 +31377,10 @@ export type operations = {
200: {
content: {
'application/json': {
- desynced: boolean;
- game?: components['schemas']['ReversiGameDetailed'] | null;
+ emojis: components['schemas']['EmojiDetailedAdmin'][];
+ count: number;
+ allCount: number;
+ allPages: number;
};
};
};
@@ -30935,12 +31408,6 @@ export type operations = {
'application/json': components['schemas']['Error'];
};
};
- /** @description Too many requests */
- 429: {
- content: {
- 'application/json': components['schemas']['Error'];
- };
- };
/** @description Internal server error */
500: {
content: {
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index 0faf3dddc4..0ce066182d 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -4,12 +4,8 @@ import type {
Ad,
Announcement,
EmojiDetailed,
- Flash,
- GalleryPost,
InviteCode,
MetaDetailed,
- Note,
- Page,
Role,
ReversiGameDetailed,
SystemWebhook,
@@ -83,11 +79,13 @@ export const permissions = [
'write:admin:decline-user',
'write:admin:nsfw-user',
'write:admin:unnsfw-user',
+ 'write:admin:cw-user',
'write:admin:silence-user',
'write:admin:unsilence-user',
'write:admin:unset-user-avatar',
'write:admin:unset-user-banner',
'write:admin:unsuspend-user',
+ 'write:admin:reject-quotes',
'write:admin:meta',
'write:admin:user-note',
'write:admin:roles',
@@ -124,6 +122,7 @@ export const moderationLogTypes = [
'updateServerSettings',
'suspend',
'approve',
+ 'decline',
'unsuspend',
'updateUserNote',
'addCustomEmoji',
@@ -145,8 +144,13 @@ export const moderationLogTypes = [
'deleteGlobalAnnouncement',
'deleteUserAnnouncement',
'resetPassword',
+ 'setMandatoryCW',
+ 'setRemoteInstanceNSFW',
+ 'unsetRemoteInstanceNSFW',
'suspendRemoteInstance',
'unsuspendRemoteInstance',
+ 'rejectRemoteInstanceReports',
+ 'acceptRemoteInstanceReports',
'updateRemoteInstanceNote',
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
@@ -186,7 +190,14 @@ export const reversiUpdateKeys = [
export type ReversiUpdateKey = typeof reversiUpdateKeys[number];
-type AvatarDecoration = UserLite['avatarDecorations'][number];
+interface AvatarDecoration {
+ id: string;
+ updatedAt: string | null;
+ url: string;
+ name: string;
+ description: string;
+ roleIdsThatCanBeUsedThisDecoration: string[];
+}
type ReceivedAbuseReport = {
reportId: AbuseReportNotificationRecipient['id'];
@@ -280,7 +291,6 @@ export type ModerationLogPayloads = {
noteUserId: string;
noteUserUsername: string;
noteUserHost: string | null;
- note: Note;
};
createGlobalAnnouncement: {
announcementId: string;
@@ -322,6 +332,21 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
+ setMandatoryCW: {
+ newCW: string | null;
+ oldCW: string | null;
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ setRemoteInstanceNSFW: {
+ id: string;
+ host: string;
+ };
+ unsetRemoteInstanceNSFW: {
+ id: string;
+ host: string;
+ };
suspendRemoteInstance: {
id: string;
host: string;
@@ -330,6 +355,14 @@ export type ModerationLogPayloads = {
id: string;
host: string;
};
+ rejectRemoteInstanceReports: {
+ id: string;
+ host: string;
+ };
+ acceptRemoteInstanceReports: {
+ id: string;
+ host: string;
+ };
updateRemoteInstanceNote: {
id: string;
host: string;
@@ -440,18 +473,98 @@ export type ModerationLogPayloads = {
pageId: string;
pageUserId: string;
pageUserUsername: string;
- page: Page;
};
deleteFlash: {
flashId: string;
flashUserId: string;
flashUserUsername: string;
- flash: Flash;
};
deleteGalleryPost: {
postId: string;
postUserId: string;
postUserUsername: string;
- post: GalleryPost;
+ };
+ acceptQuotesUser: {
+ userId: string,
+ userUsername: string,
+ userHost: string | null,
+ };
+ rejectQuotesUser: {
+ userId: string,
+ userUsername: string,
+ userHost: string | null,
+ };
+ acceptQuotesInstance: {
+ id: string;
+ host: string;
+ };
+ rejectQuotesInstance: {
+ id: string;
+ host: string;
+ };
+
+ clearUserFiles: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ count: number;
+ };
+ nsfwUser: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ unNsfwUser: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ silenceUser: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ unSilenceUser: {
+ userId: string;
+ userUsername: string;
+ userHost: string | null;
+ };
+ createAccount: {
+ userId: string;
+ userUsername: string;
+ };
+ clearRemoteFiles: Record<string, never>;
+ clearOwnerlessFiles: {
+ count: number;
+ };
+ updateCustomEmojis: {
+ ids: string[],
+ category?: string | null,
+ license?: string | null,
+ setAliases?: string[],
+ addAliases?: string[],
+ delAliases?: string[],
+ };
+ importCustomEmojis: {
+ fileName: string,
+ };
+ clearInstanceFiles: {
+ host: string;
+ count: number;
+ };
+ severFollowRelations: {
+ host: string;
+ };
+ createPromo: {
+ noteId: string,
+ noteUserId: string;
+ noteUserUsername: string;
+ noteUserHost: string | null;
+ };
+ addRelay: {
+ inbox: string;
+ };
+ removeRelay: {
+ inbox: string;
};
};
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index efe5ba19fb..3b31a6e531 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -61,6 +61,18 @@ export type ModerationLog = {
type: 'unsuspend';
info: ModerationLogPayloads['unsuspend'];
} | {
+ type: 'acceptQuotesUser';
+ info: ModerationLogPayloads['acceptQuotesUser'];
+} | {
+ type: 'rejectQuotesUser';
+ info: ModerationLogPayloads['rejectQuotesUser'];
+} | {
+ type: 'acceptQuotesInstance';
+ info: ModerationLogPayloads['acceptQuotesInstance'];
+} | {
+ type: 'rejectQuotesInstance';
+ info: ModerationLogPayloads['rejectQuotesInstance'];
+} | {
type: 'updateUserNote';
info: ModerationLogPayloads['updateUserNote'];
} | {
@@ -118,6 +130,15 @@ export type ModerationLog = {
type: 'deleteUserAnnouncement';
info: ModerationLogPayloads['deleteUserAnnouncement'];
} | {
+ type: 'setMandatoryCW';
+ info: ModerationLogPayloads['setMandatoryCW'];
+} | {
+ type: 'setRemoteInstanceNSFW';
+ info: ModerationLogPayloads['setRemoteInstanceNSFW'];
+} | {
+ type: 'unsetRemoteInstanceNSFW';
+ info: ModerationLogPayloads['unsetRemoteInstanceNSFW'];
+} | {
type: 'resetPassword';
info: ModerationLogPayloads['resetPassword'];
} | {
@@ -127,6 +148,12 @@ export type ModerationLog = {
type: 'unsuspendRemoteInstance';
info: ModerationLogPayloads['unsuspendRemoteInstance'];
} | {
+ type: 'rejectRemoteInstanceReports';
+ info: ModerationLogPayloads['rejectRemoteInstanceReports'];
+} | {
+ type: 'acceptRemoteInstanceReports';
+ info: ModerationLogPayloads['acceptRemoteInstanceReports'];
+} | {
type: 'updateRemoteInstanceNote';
info: ModerationLogPayloads['updateRemoteInstanceNote'];
} | {
@@ -201,6 +228,51 @@ export type ModerationLog = {
} | {
type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost'];
+} | {
+ type: 'clearUserFiles';
+ info: ModerationLogPayloads['clearUserFiles'];
+} | {
+ type: 'nsfwUser';
+ info: ModerationLogPayloads['nsfwUser'];
+} | {
+ type: 'unNsfwUser';
+ info: ModerationLogPayloads['unNsfwUser'];
+} | {
+ type: 'silenceUser';
+ info: ModerationLogPayloads['silenceUser'];
+} | {
+ type: 'unSilenceUser';
+ info: ModerationLogPayloads['unSilenceUser'];
+} | {
+ type: 'createAccount';
+ info: ModerationLogPayloads['createAccount'];
+} | {
+ type: 'clearRemoteFiles';
+ info: ModerationLogPayloads['clearRemoteFiles'];
+} | {
+ type: 'clearOwnerlessFiles';
+ info: ModerationLogPayloads['clearOwnerlessFiles'];
+} | {
+ type: 'updateCustomEmojis';
+ info: ModerationLogPayloads['updateCustomEmojis'];
+} | {
+ type: 'importCustomEmojis';
+ info: ModerationLogPayloads['importCustomEmojis'];
+} | {
+ type: 'clearInstanceFiles';
+ info: ModerationLogPayloads['clearInstanceFiles'];
+} | {
+ type: 'severFollowRelations';
+ info: ModerationLogPayloads['severFollowRelations'];
+} | {
+ type: 'createPromo';
+ info: ModerationLogPayloads['createPromo'];
+} | {
+ type: 'addRelay';
+ info: ModerationLogPayloads['addRelay'];
+} | {
+ type: 'removeRelay';
+ info: ModerationLogPayloads['removeRelay'];
});
export type ServerStats = {
diff --git a/packages/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts
index 6e34ec1508..0ef2d1e7a1 100644
--- a/packages/misskey-js/src/streaming.ts
+++ b/packages/misskey-js/src/streaming.ts
@@ -1,8 +1,10 @@
import { EventEmitter } from 'eventemitter3';
-import _ReconnectingWebsocket from 'reconnecting-websocket';
+import _ReconnectingWebSocket, { Options } from 'reconnecting-websocket';
import type { BroadcastEvents, Channels } from './streaming.types.js';
-const ReconnectingWebsocket = _ReconnectingWebsocket as unknown as typeof _ReconnectingWebsocket['default'];
+// コンストラクタとクラスそのものの定義が上手く解決出来ないため再定義
+const ReconnectingWebSocketConstructor = _ReconnectingWebSocket as unknown as typeof _ReconnectingWebSocket.default;
+type ReconnectingWebSocket = _ReconnectingWebSocket.default;
export function urlQuery(obj: Record<string, string | number | boolean | undefined>): string {
const params = Object.entries(obj)
@@ -43,7 +45,7 @@ export interface IStream extends EventEmitter<StreamEvents> {
*/
// eslint-disable-next-line import/no-default-export
export default class Stream extends EventEmitter<StreamEvents> implements IStream {
- private stream: _ReconnectingWebsocket.default;
+ private stream: ReconnectingWebSocket;
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
private sharedConnectionPools: Pool[] = [];
private sharedConnections: SharedConnection[] = [];
@@ -51,7 +53,8 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
private idCounter = 0;
constructor(origin: string, user: { token: string; } | null, options?: {
- WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
+ WebSocket?: Options['WebSocket'];
+ binaryType?: ReconnectingWebSocket['binaryType'];
}) {
super();
@@ -80,10 +83,13 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
const wsOrigin = origin.replace('http://', 'ws://').replace('https://', 'wss://');
- this.stream = new ReconnectingWebsocket(`${wsOrigin}/streaming?${query}`, '', {
+ this.stream = new ReconnectingWebSocketConstructor(`${wsOrigin}/streaming?${query}`, '', {
minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91
WebSocket: options.WebSocket,
});
+ if (options.binaryType) {
+ this.stream.binaryType = options.binaryType;
+ }
this.stream.addEventListener('open', this.onOpen);
this.stream.addEventListener('close', this.onClose);
this.stream.addEventListener('message', this.onMessage);
diff --git a/packages/misskey-reversi/build.js b/packages/misskey-reversi/build.js
index a80b71646f..5d534cc6fd 100644
--- a/packages/misskey-reversi/build.js
+++ b/packages/misskey-reversi/build.js
@@ -23,10 +23,14 @@ const options = {
sourcemap: 'linked',
};
+const args = process.argv.slice(2).map(arg => arg.toLowerCase());
+
// built配下をすべて削除する
-fs.rmSync('./built', { recursive: true, force: true });
+if (!args.includes('--no-clean')) {
+ fs.rmSync('./built', { recursive: true, force: true });
+}
-if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
+if (args.includes('--watch')) {
await watchSrc();
} else {
await buildSrc();
diff --git a/packages/shared/eslint.config.js b/packages/shared/eslint.config.js
index 0368d008c0..860eb4a8e8 100644
--- a/packages/shared/eslint.config.js
+++ b/packages/shared/eslint.config.js
@@ -32,4 +32,11 @@ export default [
'@typescript-eslint/no-var-requires': 'off',
},
},
+ {
+ rules: {
+ 'no-restricted-imports': ['error', {
+ paths: [{ name: 'punycode' }],
+ }],
+ },
+ },
];
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index beb051af19..1c2b563cf1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -128,9 +128,6 @@ importers:
'@misskey-dev/summaly':
specifier: 5.1.0
version: 5.1.0
- '@napi-rs/canvas':
- specifier: 0.1.56
- version: 0.1.56
'@nestjs/common':
specifier: 10.4.7
version: 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -139,7 +136,7 @@ importers:
version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/testing':
specifier: 10.4.7
- version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)
+ version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7))
'@peertube/http-signature':
specifier: 1.7.0
version: 1.7.0
@@ -203,6 +200,9 @@ importers:
cacheable-lookup:
specifier: 7.0.0
version: 7.0.0
+ canvas:
+ specifier: ^3.1.0
+ version: 3.1.0
cbor:
specifier: 9.0.2
version: 9.0.2
@@ -289,7 +289,7 @@ importers:
version: 4.1.0
jsdom:
specifier: 24.1.1
- version: 24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+ version: 24.1.1(bufferutil@4.0.7)(canvas@3.1.0)(utf-8-validate@6.0.3)
json5:
specifier: 2.2.3
version: 2.2.3
@@ -377,9 +377,6 @@ importers:
pug:
specifier: 3.0.3
version: 3.0.3
- punycode:
- specifier: 2.3.1
- version: 2.3.1
qrcode:
specifier: 1.5.4
version: 1.5.4
@@ -631,9 +628,6 @@ importers:
'@types/pug':
specifier: 2.0.10
version: 2.0.10
- '@types/punycode':
- specifier: 2.1.4
- version: 2.1.4
'@types/qrcode':
specifier: 1.5.5
version: 1.5.5
@@ -758,8 +752,8 @@ importers:
specifier: 3.5.12
version: 3.5.12
aiscript-vscode:
- specifier: github:aiscript-dev/aiscript-vscode#v0.1.11
- version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9
+ specifier: github:aiscript-dev/aiscript-vscode#v0.1.15
+ version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7
astring:
specifier: 1.9.0
version: 1.9.0
@@ -841,7 +835,7 @@ importers:
photoswipe:
specifier: 5.4.4
version: 5.4.4
- punycode:
+ punycode.js:
specifier: 2.3.1
version: 2.3.1
rollup:
@@ -978,9 +972,9 @@ importers:
'@types/node':
specifier: 22.9.0
version: 22.9.0
- '@types/punycode':
- specifier: 2.1.4
- version: 2.1.4
+ '@types/punycode.js':
+ specifier: npm:@types/punycode@2.1.4
+ version: '@types/punycode@2.1.4'
'@types/sanitize-html':
specifier: 2.13.0
version: 2.13.0
@@ -1131,7 +1125,7 @@ importers:
misskey-js:
specifier: workspace:*
version: link:../misskey-js
- punycode:
+ punycode.js:
specifier: 2.3.1
version: 2.3.1
rollup:
@@ -1180,9 +1174,9 @@ importers:
'@types/node':
specifier: 22.9.0
version: 22.9.0
- '@types/punycode':
- specifier: 2.1.4
- version: 2.1.4
+ '@types/punycode.js':
+ specifier: npm:@types/punycode@2.1.4
+ version: '@types/punycode@2.1.4'
'@types/tinycolor2':
specifier: 1.4.6
version: 1.4.6
@@ -1200,7 +1194,7 @@ importers:
version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
'@vitest/coverage-v8':
specifier: 1.6.0
- version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))
+ version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0))
'@vue/runtime-core':
specifier: 3.5.12
version: 3.5.12
@@ -1258,6 +1252,9 @@ importers:
misskey-js:
specifier: workspace:*
version: link:../misskey-js
+ nodemon:
+ specifier: 3.1.7
+ version: 3.1.7
vue:
specifier: 3.5.12
version: 3.5.12(typescript@5.6.3)
@@ -1496,6 +1493,9 @@ importers:
'@typescript-eslint/parser':
specifier: 7.17.0
version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+ eslint:
+ specifier: 9.14.0
+ version: 9.14.0
openapi-types:
specifier: 12.1.3
version: 12.1.3
@@ -3007,64 +3007,6 @@ packages:
resolution: {integrity: sha512-GXrJgakgJW3DWKueebkvtYgGKkxA7s0u5B0P5syJM5rvQUnrpLPigvci8Hukl7yEM+sU06l+er2Fgvx/gmiRgg==}
engines: {node: '>=18'}
- '@napi-rs/canvas-android-arm64@0.1.56':
- resolution: {integrity: sha512-xBGqW2RZMAupkzar9t3gpbok9r524f3Wlk4PG2qnQdxbsiEND06OB8VxVtTcql6R02uJpXJGnyIhN02Te+GMVQ==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [android]
-
- '@napi-rs/canvas-darwin-arm64@0.1.56':
- resolution: {integrity: sha512-Pvuz6Ib9YZTB5MlGL9WSu9a2asUC0DZ1zBHozDiBXr/6Zurs9l/ZH5NxFYTM829BpkdkO8kuI8b8Rz7ek30zzQ==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [darwin]
-
- '@napi-rs/canvas-darwin-x64@0.1.56':
- resolution: {integrity: sha512-O393jWt7G6rg0X1ralbsbBeskSG0iwlkD7mEHhMLJxqRqe+eQn0/xnwhs9l6dUNFC+5dM8LOvfFca4o9Vs2Vww==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [darwin]
-
- '@napi-rs/canvas-linux-arm-gnueabihf@0.1.56':
- resolution: {integrity: sha512-30NFb5lrF3YEwAO5XuATxpWDSXaBAgaFVswPJ+hYcAUyE3IkPPIFRY4ijQEh4frcSBvrzFGGYdNSoC18oLLWaQ==}
- engines: {node: '>= 10'}
- cpu: [arm]
- os: [linux]
-
- '@napi-rs/canvas-linux-arm64-gnu@0.1.56':
- resolution: {integrity: sha512-ODbWH9TLvba+39UxFwPn2Hm1ImALmWOZ0pEv5do/pz0439326Oz49hlfGot4KmkSBeKK81knWxRj9EXMSPwXPg==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [linux]
-
- '@napi-rs/canvas-linux-arm64-musl@0.1.56':
- resolution: {integrity: sha512-zqE4nz8CWiJJ0q5By7q9CDPicNkc0oyErgavK3ZV279zJL7Aapd3cIqayT6ynECArg7GgBl2WYSvr5AaRFmYgg==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [linux]
-
- '@napi-rs/canvas-linux-x64-gnu@0.1.56':
- resolution: {integrity: sha512-JTnGAtJBQMhfSpN8/rbMnf5oxuO/juUNa0n4LA0LlW0JS9UBpmsS2BwFNCakFqOeAPaqIM6sFFsK3M4hve+Esw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [linux]
-
- '@napi-rs/canvas-linux-x64-musl@0.1.56':
- resolution: {integrity: sha512-mpws7DhVDIj8ZKa/qcnUVLAm0fxD9RK5ojfNNSI9TOzn2E0f+GUXx8sGsCxDpMVMtN+mtyrMwRqH3F3rTUMWXw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [linux]
-
- '@napi-rs/canvas-win32-x64-msvc@0.1.56':
- resolution: {integrity: sha512-VKAAkgXF+lbFvRFawPOtkfV/P7ogAgWTu5FMCIiBn0Gc3vnkKFG2cLo/IHIJ7FuriToKEidkJGT88iAh7W7GDA==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [win32]
-
- '@napi-rs/canvas@0.1.56':
- resolution: {integrity: sha512-SujSchzG6lLc/wT+Mwxam/w30Kk2sFTiU6bLFcidecKSmlhenAhGMQhZh2iGFfKoh2+8iit0jrt99n6TqReICQ==}
- engines: {node: '>= 10'}
-
'@nestjs/common@10.4.7':
resolution: {integrity: sha512-gIOpjD3Mx8gfYGxYm/RHPcJzqdknNNFCyY+AxzBT3gc5Xvvik1Dn5OxaMGw5EbVfhZgJKVP0n83giUOAlZQe7w==}
peerDependencies:
@@ -5070,9 +5012,9 @@ packages:
resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==}
engines: {node: '>=18'}
- aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9:
- resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9}
- version: 0.1.11
+ aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7:
+ resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7}
+ version: 0.1.15
engines: {vscode: ^1.83.0}
ajv-draft-04@1.0.0:
@@ -5382,6 +5324,9 @@ packages:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
+ bl@4.1.0:
+ resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+
blob-util@2.0.2:
resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
@@ -5558,6 +5503,10 @@ packages:
canvas-confetti@1.9.3:
resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==}
+ canvas@3.1.0:
+ resolution: {integrity: sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==}
+ engines: {node: ^18.12.0 || >= 20.9.0}
+
caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
@@ -5663,6 +5612,9 @@ packages:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
+ chownr@1.1.4:
+ resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
+
chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
@@ -6108,6 +6060,10 @@ packages:
deep-equal@2.2.0:
resolution: {integrity: sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==}
+ deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -6600,6 +6556,10 @@ packages:
resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
engines: {node: '>= 0.8.0'}
+ expand-template@2.0.3:
+ resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
+ engines: {node: '>=6'}
+
expect@29.7.0:
resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -6861,6 +6821,9 @@ packages:
from@0.1.7:
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
+ fs-constants@1.0.0:
+ resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+
fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
@@ -6962,6 +6925,9 @@ packages:
getpass@0.1.7:
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
+ github-from-package@0.0.0:
+ resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
+
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -8400,6 +8366,9 @@ packages:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
+ mkdirp-classic@0.5.3:
+ resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
@@ -8496,6 +8465,9 @@ packages:
engines: {node: ^18 || >=20}
hasBin: true
+ napi-build-utils@2.0.0:
+ resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -9271,6 +9243,11 @@ packages:
postgres-range@1.1.3:
resolution: {integrity: sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==}
+ prebuild-install@7.1.3:
+ resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
+ engines: {node: '>=10'}
+ hasBin: true
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -9412,6 +9389,10 @@ packages:
pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -9477,6 +9458,10 @@ packages:
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
engines: {node: '>= 0.8'}
+ rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+
rdf-canonize@3.4.0:
resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==}
engines: {node: '>=12'}
@@ -9866,6 +9851,12 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
+ simple-concat@1.0.1:
+ resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
+
+ simple-get@4.0.1:
+ resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
+
simple-oauth2@5.1.0:
resolution: {integrity: sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==}
@@ -10229,6 +10220,10 @@ packages:
resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==}
engines: {node: '>=12'}
+ strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -10295,6 +10290,13 @@ packages:
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
+ tar-fs@2.1.2:
+ resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==}
+
+ tar-stream@2.2.0:
+ resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
+ engines: {node: '>=6'}
+
tar-stream@3.1.6:
resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==}
@@ -10967,6 +10969,9 @@ packages:
vue-component-type-helpers@2.1.10:
resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==}
+ vue-component-type-helpers@2.2.0:
+ resolution: {integrity: sha512-cYrAnv2me7bPDcg9kIcGwjJiSB6Qyi08+jLDo9yuvoFQjzHiPTzML7RnkJB1+3P6KMsX/KbCD4QE3Tv/knEllw==}
+
vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'}
@@ -13198,45 +13203,6 @@ snapshots:
outvariant: 1.4.3
strict-event-emitter: 0.5.1
- '@napi-rs/canvas-android-arm64@0.1.56':
- optional: true
-
- '@napi-rs/canvas-darwin-arm64@0.1.56':
- optional: true
-
- '@napi-rs/canvas-darwin-x64@0.1.56':
- optional: true
-
- '@napi-rs/canvas-linux-arm-gnueabihf@0.1.56':
- optional: true
-
- '@napi-rs/canvas-linux-arm64-gnu@0.1.56':
- optional: true
-
- '@napi-rs/canvas-linux-arm64-musl@0.1.56':
- optional: true
-
- '@napi-rs/canvas-linux-x64-gnu@0.1.56':
- optional: true
-
- '@napi-rs/canvas-linux-x64-musl@0.1.56':
- optional: true
-
- '@napi-rs/canvas-win32-x64-msvc@0.1.56':
- optional: true
-
- '@napi-rs/canvas@0.1.56':
- optionalDependencies:
- '@napi-rs/canvas-android-arm64': 0.1.56
- '@napi-rs/canvas-darwin-arm64': 0.1.56
- '@napi-rs/canvas-darwin-x64': 0.1.56
- '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.56
- '@napi-rs/canvas-linux-arm64-gnu': 0.1.56
- '@napi-rs/canvas-linux-arm64-musl': 0.1.56
- '@napi-rs/canvas-linux-x64-gnu': 0.1.56
- '@napi-rs/canvas-linux-x64-musl': 0.1.56
- '@napi-rs/canvas-win32-x64-msvc': 0.1.56
-
'@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)':
dependencies:
iterare: 1.2.1
@@ -13273,7 +13239,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)':
+ '@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7))':
dependencies:
'@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -14641,7 +14607,7 @@ snapshots:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.5.12(typescript@5.6.3)
- vue-component-type-helpers: 2.1.10
+ vue-component-type-helpers: 2.2.0
'@swc/cli@0.3.12(@swc/core@1.9.2)(chokidar@3.5.3)':
dependencies:
@@ -15494,7 +15460,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))':
+ '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0))':
dependencies:
'@ampproject/remapping': 2.2.1
'@bcoe/v8-coverage': 0.2.3
@@ -15509,7 +15475,7 @@ snapshots:
std-env: 3.7.0
strip-literal: 2.1.0
test-exclude: 6.0.0
- vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0)
+ vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0)
transitivePeerDependencies:
- supports-color
@@ -15751,7 +15717,7 @@ snapshots:
clean-stack: 5.2.0
indent-string: 5.0.0
- aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9:
+ aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7:
dependencies:
'@aiscript-dev/aiscript-languageserver': https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz
vscode-languageclient: 9.0.1
@@ -16175,6 +16141,12 @@ snapshots:
binary-extensions@2.2.0: {}
+ bl@4.1.0:
+ dependencies:
+ buffer: 5.7.1
+ inherits: 2.0.4
+ readable-stream: 3.6.0
+
blob-util@2.0.2:
optional: true
@@ -16271,7 +16243,6 @@ snapshots:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
- optional: true
buffer@6.0.3:
dependencies:
@@ -16404,6 +16375,11 @@ snapshots:
canvas-confetti@1.9.3: {}
+ canvas@3.1.0:
+ dependencies:
+ node-addon-api: 7.1.0
+ prebuild-install: 7.1.3
+
caseless@0.12.0:
optional: true
@@ -16540,6 +16516,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ chownr@1.1.4: {}
+
chownr@2.0.0: {}
chromatic@11.18.1: {}
@@ -17050,6 +17028,8 @@ snapshots:
which-collection: 1.0.1
which-typed-array: 1.1.11
+ deep-extend@0.6.0: {}
+
deep-is@0.1.4: {}
deepmerge@4.2.2: {}
@@ -17809,6 +17789,8 @@ snapshots:
exit@0.1.2: {}
+ expand-template@2.0.3: {}
+
expect@29.7.0:
dependencies:
'@jest/expect-utils': 29.7.0
@@ -18181,6 +18163,8 @@ snapshots:
from@0.1.7: {}
+ fs-constants@1.0.0: {}
+
fs-extra@7.0.1:
dependencies:
graceful-fs: 4.2.11
@@ -18294,6 +18278,8 @@ snapshots:
dependencies:
assert-plus: 1.0.0
+ github-from-package@0.0.0: {}
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -19352,7 +19338,36 @@ snapshots:
jsdoc-type-pratt-parser@4.1.0: {}
- jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+ jsdom@24.1.1:
+ dependencies:
+ cssstyle: 4.0.1
+ data-urls: 5.0.0
+ decimal.js: 10.4.3
+ form-data: 4.0.1
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.5
+ is-potential-custom-element-name: 1.0.1
+ nwsapi: 2.2.12
+ parse5: 7.2.1
+ rrweb-cssom: 0.7.1
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 4.1.4
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.0.0
+ ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ optional: true
+
+ jsdom@24.1.1(bufferutil@4.0.7)(canvas@3.1.0)(utf-8-validate@6.0.3):
dependencies:
cssstyle: 4.0.1
data-urls: 5.0.0
@@ -19375,6 +19390,8 @@ snapshots:
whatwg-url: 14.0.0
ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
xml-name-validator: 5.0.0
+ optionalDependencies:
+ canvas: 3.1.0
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -20169,6 +20186,8 @@ snapshots:
minipass: 3.3.6
yallist: 4.0.0
+ mkdirp-classic@0.5.3: {}
+
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
@@ -20276,6 +20295,8 @@ snapshots:
nanoid@5.0.8: {}
+ napi-build-utils@2.0.0: {}
+
natural-compare@1.4.0: {}
ncp@2.0.0: {}
@@ -21018,6 +21039,21 @@ snapshots:
postgres-range@1.1.3: {}
+ prebuild-install@7.1.3:
+ dependencies:
+ detect-libc: 2.0.3
+ expand-template: 2.0.3
+ github-from-package: 0.0.0
+ minimist: 1.2.8
+ mkdirp-classic: 0.5.3
+ napi-build-utils: 2.0.0
+ node-abi: 3.62.0
+ pump: 3.0.0
+ rc: 1.2.8
+ simple-get: 4.0.1
+ tar-fs: 2.1.2
+ tunnel-agent: 0.6.0
+
prelude-ls@1.2.1: {}
prettier@3.3.3: {}
@@ -21186,6 +21222,8 @@ snapshots:
end-of-stream: 1.4.4
once: 1.4.0
+ punycode.js@2.3.1: {}
+
punycode@2.3.1: {}
pure-rand@6.0.0: {}
@@ -21242,6 +21280,13 @@ snapshots:
iconv-lite: 0.6.3
unpipe: 1.0.0
+ rc@1.2.8:
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.8
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+
rdf-canonize@3.4.0:
dependencies:
setimmediate: 1.0.5
@@ -21749,6 +21794,14 @@ snapshots:
signal-exit@4.1.0: {}
+ simple-concat@1.0.1: {}
+
+ simple-get@4.0.1:
+ dependencies:
+ decompress-response: 6.0.0
+ once: 1.4.0
+ simple-concat: 1.0.1
+
simple-oauth2@5.1.0:
dependencies:
'@hapi/hoek': 11.0.4
@@ -22129,6 +22182,8 @@ snapshots:
dependencies:
min-indent: 1.0.1
+ strip-json-comments@2.0.1: {}
+
strip-json-comments@3.1.1: {}
strip-literal@2.1.0:
@@ -22190,6 +22245,21 @@ snapshots:
systeminformation@5.23.5: {}
+ tar-fs@2.1.2:
+ dependencies:
+ chownr: 1.1.4
+ mkdirp-classic: 0.5.3
+ pump: 3.0.0
+ tar-stream: 2.2.0
+
+ tar-stream@2.2.0:
+ dependencies:
+ bl: 4.1.0
+ end-of-stream: 1.4.4
+ fs-constants: 1.0.0
+ inherits: 2.0.4
+ readable-stream: 3.6.0
+
tar-stream@3.1.6:
dependencies:
b4a: 1.6.4
@@ -22416,7 +22486,6 @@ snapshots:
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
- optional: true
tweetnacl@0.14.5: {}
@@ -22827,7 +22896,7 @@ snapshots:
- supports-color
- terser
- vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0):
+ vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0):
dependencies:
'@vitest/expect': 1.6.0
'@vitest/runner': 1.6.0
@@ -22852,7 +22921,7 @@ snapshots:
optionalDependencies:
'@types/node': 22.9.0
happy-dom: 10.0.3
- jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ jsdom: 24.1.1
transitivePeerDependencies:
- less
- lightningcss
@@ -22907,6 +22976,8 @@ snapshots:
vue-component-type-helpers@2.1.10: {}
+ vue-component-type-helpers@2.2.0: {}
+
vue-demi@0.14.7(vue@3.5.12(typescript@5.6.3)):
dependencies:
vue: 3.5.12(typescript@5.6.3)
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
index ea3b017ba5..96984eea35 100644
--- a/scripts/dev.mjs
+++ b/scripts/dev.mjs
@@ -27,7 +27,7 @@ await Promise.all([
stdout: process.stdout,
stderr: process.stderr,
}),
- execa('pnpm', ['--filter', 'misskey-js', 'build'], {
+ execa('pnpm', ['--filter', 'backend...', 'build'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
@@ -70,19 +70,19 @@ execa('pnpm', ['--filter', 'backend', 'dev'], {
stderr: process.stderr,
});
-execa('pnpm', ['--filter', 'frontend-shared', 'watch'], {
+execa('pnpm', ['--filter', 'frontend-shared', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
-execa('pnpm', ['--filter', 'frontend', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], {
+execa('pnpm', ['--filter', 'frontend', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
-execa('pnpm', ['--filter', 'frontend-embed', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], {
+execa('pnpm', ['--filter', 'frontend-embed', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
@@ -94,19 +94,19 @@ execa('pnpm', ['--filter', 'sw', 'watch'], {
stderr: process.stderr,
});
-execa('pnpm', ['--filter', 'misskey-js', 'watch'], {
+execa('pnpm', ['--filter', 'misskey-js', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
-execa('pnpm', ['--filter', 'misskey-reversi', 'watch'], {
+execa('pnpm', ['--filter', 'misskey-reversi', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
-execa('pnpm', ['--filter', 'misskey-bubble-game', 'watch'], {
+execa('pnpm', ['--filter', 'misskey-bubble-game', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml
index db3ef9c3eb..9a41a9d762 100644
--- a/sharkey-locales/en-US.yml
+++ b/sharkey-locales/en-US.yml
@@ -17,6 +17,7 @@ emailDestination: "Destination address"
date: "Date"
renote: "Boost"
unrenote: "Remove boost"
+renoteShift: "Boost (hold Shift for visibility menu)"
renoted: "Boosted."
quoted: "Quoted."
rmboost: "Unboosted."
@@ -40,6 +41,11 @@ continueOnRemote: "Continue on remote instance"
chooseServerOnMisskeyHub: "Choose a instance from Misskey Hub"
mediaSilenceThisInstance: "Silence media from this instance"
rejectReports: "Reject reports from this instance"
+rejectQuotesInstance: "Strip quote posts from this instance"
+rejectQuotesRemoteUser: "Strip quote posts from this user"
+rejectQuotesLocalUser: "Block quote posts from this user"
+rejectQuotesConfirm: "Are you sure you wish to strip quote posts?"
+allowQuotesConfirm: "Are you sure you wish to allow quote posts?"
silencedInstancesDescription: "List the host names of the instances that you want to silence, separated by a new line. All accounts belonging to the listed instances will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
mediaSilencedInstances: "Media-silenced instances"
mediaSilencedInstancesDescription: "List the host names of the instances that you want to media-silence, separated by a new line. All accounts belonging to the listed instances will be treated as sensitive, and can't use custom emojis. This will not affect the blocked instances."
@@ -50,6 +56,10 @@ mediaSilencedByBase: "This host's media is silenced implicitly because a base do
driveSearchbarPlaceholder: "Search drive"
fileNotSelected: "No file selected"
background: "Background"
+inputNewDescription: "Enter new alt text"
+describeFile: "Add alt text"
+enterFileDescription: "Enter alt text"
+caption: "Alt text"
antennaUsersDescription: "List one username per line. Use \"*@instance.com\" to specify all users of an instance"
aboutMisskey: "About Sharkey"
expandAllCws: "Show content for all replies"
@@ -159,6 +169,9 @@ pinnedOnly: "Pinned"
blockingYou: "Blocking you"
warnExternalUrl: "Show warning when opening external URLs"
flash: "Flash"
+filesRemoved: "Files removed"
+fileImported: "File imported"
+cannotLoadNote: "Failed to load note"
_flash:
contentHidden: "Flash Content Hidden"
poweredByRuffle: "Powered by Ruffle."
@@ -270,6 +283,7 @@ _profile:
changeBackground: "Change background"
updateBackground: "Update background"
removeBackground: "Remove background"
+ listenbrainz: "ListenBrainz username"
_timelines:
bubble: "Bubble"
_pages:
@@ -301,10 +315,30 @@ _abuseReport:
webhook: "Send a notification to the SystemWebhook when an abuse report is received or resolved."
_moderationLogTypes:
approve: "Approved"
+ decline: "Declined"
+ setMandatoryCW: "Set content warning for user"
setRemoteInstanceNSFW: "Set remote instance as NSFW"
unsetRemoteInstanceNSFW: "Set remote instance as NSFW"
rejectRemoteInstanceReports: "Rejected reports from remote instance"
acceptRemoteInstanceReports: "Accepted reports from remote instance"
+ rejectQuotesUser: "Blocked/Stripped quote posts from user"
+ allowQuotesUser: "Allowed quote posts from user"
+ clearUserFiles: "Cleared a user's drive files"
+ nsfwUser: "Marked user as NSFW"
+ unNsfwUser: "Un-marked user as NSFW"
+ silenceUser: "Silenced user"
+ unSilenceUser: "Un-silenced user"
+ createAccount: "Created an account"
+ clearRemoteFiles: "Cleared remote drive files"
+ clearOwnerlessFiles: "Cleared owner-less drive files"
+ updateCustomEmojis: "Updated custom emojis"
+ importCustomEmojis: "Imported custom emojis"
+ clearInstanceFiles: "Cleared an instance's drive files"
+ severFollowRelations: "Severed follow relations with an instance"
+ createPromo: "Created a note promo"
+ addRelay: "Added a relay"
+ removeRelay: "Removed a relay"
+
_mfm:
uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks"
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
@@ -383,6 +417,8 @@ _mfm:
fadeDescription: 'Fade text in and out.'
background: "Background color"
backgroundDescription: "Change the background color of text."
+ border: "Border"
+ borderDescription: "Draw a border around the content."
plain: "Plain"
plainDescription: "Deactivates the effects of all MFM contained within this MFM effect."
_animatedMFM:
@@ -428,5 +464,50 @@ postOn: "Post on"
scheduledNotes: "Scheduled Notes"
_permissions:
+ "write:admin:approve-user": "Approve new users"
+ "write:admin:decline-user": "Decline new users"
+ "write:admin:nsfw-user": "Mark users as NSFW"
+ "write:admin:unnsfw-user": "Mark users an not NSFW"
+ "write:admin:cw-user": "Apply mandatory CW on users"
+ "write:admin:silence-user": "Silence users"
+ "write:admin:unsilence-user": "Un-silence users"
+ "write:admin:reject-quotes": "Allow/Prohibit quote posts from a user"
"read:notes-schedule": "View your list of scheduled notes"
"write:notes-schedule": "Compose or delete scheduled notes"
+
+robotsTxt: "Custom robots.txt"
+robotsTxtDescription: "Adding entries here will override the default robots.txt packaged with Sharkey."
+
+defaultCW: "Default content warning for new posts"
+defaultCWDescription: "The value here will be auto-filled as the content warning for all new posts and replies."
+defaultCWPriority: "Automatic CW priority"
+defaultCWPriorityDescription: "Select preferred action when default CW and keep CW settings are both enabled at the same time."
+_defaultCWPriority:
+ default: "Use Default (use the default CW, ignoring the inherited CW)"
+ parent: "Use Parent (use the inherited CW, ignoring the default CW)"
+ defaultParent: "Use Default, then Parent (use the default CW, and append the inherited CW)"
+ parentDefault: "Use Parent, then Default (use the inherited CW, and append the default CW)"
+
+genKeys: "Generate Keys"
+_genKeysDialog:
+ text: "Are you sure that you want to generate new keys? This will stop push notifications for all users who have already enabled them."
+ title: "Generate new keys"
+
+_noteSearch:
+ newestToOldest: "Sort by newest to oldest"
+ fileType: "File Type"
+ _fileType:
+ none: "None"
+ image: "Images"
+ video: "Videos"
+ audio: "Audio"
+ module: "Module"
+ flash: "Flash"
+
+id: "ID"
+
+mandatoryCW: "Force content warning"
+mandatoryCWDescription: "Applies a content warning to all posts created by this user. If the post already has a CW, then this is appended to the end."
+
+_processErrors:
+ quoteUnavailable: "Unable to process quote. This post may be missing context."